3 Commits

Author SHA1 Message Date
hikari 010b4ea1da feat: support runestone rewards in achievement system (#190)
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m4s
CI / Lint, Build & Test (pull_request) Successful in 1m11s
- Add runestones field to AchievementReward type
- Update tick engine to accumulate and apply runestone rewards
  when achievements unlock, alongside the existing crystal rewards
2026-03-31 18:00:21 -07:00
hikari 218a150540 feat: add missing achievement milestones and fix reward types (#188, #189, #190)
- Add quest_hero milestone at 75 quests (closes gap between 50 and 122)
- Add boss_legend milestone at 50 bosses (closes gap between 30 and 72)
- Replace crystal rewards on P50/P100/P150/P200 prestige achievements
  with runestones (100/500/2000/10000) — crystals become worthless by
  the time these are earned, runestones remain meaningful throughout
2026-03-31 18:00:16 -07:00
hikari ac42da4c3b balance: zero out crystal rewards for Zone 7+ bosses (#187)
Crystals are an early-game currency. The total crystal sink is ~125M
crystals (purchasable equipment), but Zone 7+ bosses were awarding up
to 5e139 crystals with the crystal multipliers applied. Bosses in
celestial_reaches and beyond now award 0 crystals, keeping the
crystal economy meaningful in early-mid game only.
2026-03-31 18:00:06 -07:00
4 changed files with 96 additions and 72 deletions
+22 -4
View File
@@ -315,6 +315,15 @@ export const defaultAchievements: Array<Achievement> = [
reward: { crystals: 5000 },
unlockedAt: null,
},
{
condition: { amount: 75, type: "questsCompleted" },
description: "Complete 75 quests.",
icon: "🌠",
id: "quest_hero",
name: "Quest Hero",
reward: { crystals: 10_000 },
unlockedAt: null,
},
{
condition: { amount: 122, type: "questsCompleted" },
description: "Complete all 122 quests across the known multiverse.",
@@ -343,6 +352,15 @@ export const defaultAchievements: Array<Achievement> = [
reward: { crystals: 5000 },
unlockedAt: null,
},
{
condition: { amount: 50, type: "bossesDefeated" },
description: "Defeat 50 bosses.",
icon: "⚡",
id: "boss_legend",
name: "Legendary Vanquisher",
reward: { crystals: 15_000 },
unlockedAt: null,
},
{
condition: { amount: 72, type: "bossesDefeated" },
description: "Defeat all 72 bosses across every plane of existence.",
@@ -396,7 +414,7 @@ export const defaultAchievements: Array<Achievement> = [
icon: "✨",
id: "prestige_transcendent",
name: "Transcendent",
reward: { crystals: 10_000 },
reward: { runestones: 100 },
unlockedAt: null,
},
{
@@ -405,7 +423,7 @@ export const defaultAchievements: Array<Achievement> = [
icon: "💎",
id: "prestige_eternal",
name: "Eternal Looper",
reward: { crystals: 25_000 },
reward: { runestones: 500 },
unlockedAt: null,
},
{
@@ -414,7 +432,7 @@ export const defaultAchievements: Array<Achievement> = [
icon: "🌟",
id: "prestige_immortal",
name: "Immortal Cycler",
reward: { crystals: 50_000 },
reward: { runestones: 2000 },
unlockedAt: null,
},
{
@@ -423,7 +441,7 @@ export const defaultAchievements: Array<Achievement> = [
icon: "👑",
id: "prestige_absolute",
name: "Absolute Champion",
reward: { crystals: 100_000 },
reward: { runestones: 10_000 },
unlockedAt: null,
},
];
+53 -53
View File
@@ -360,7 +360,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 40,
crystalReward: 40_000,
crystalReward: 0,
currentHp: 2_000_000_000,
damagePerSecond: 120_000,
description:
@@ -378,7 +378,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 50,
crystalReward: 100_000,
crystalReward: 0,
currentHp: 8_000_000_000,
damagePerSecond: 350_000,
description:
@@ -396,7 +396,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 60,
crystalReward: 300_000,
crystalReward: 0,
currentHp: 30_000_000_000,
damagePerSecond: 1_000_000,
description:
@@ -414,7 +414,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 75,
crystalReward: 800_000,
crystalReward: 0,
currentHp: 100_000_000_000,
damagePerSecond: 3_000_000,
description:
@@ -433,7 +433,7 @@ export const defaultBosses: Array<Boss> = [
// ── Abyssal Trench ────────────────────────────────────────────────────────
{
bountyRunestones: 40,
crystalReward: 1_500_000,
crystalReward: 0,
currentHp: 250_000_000_000,
damagePerSecond: 5_000_000,
description:
@@ -451,7 +451,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 55,
crystalReward: 4_000_000,
crystalReward: 0,
currentHp: 1_000_000_000_000,
damagePerSecond: 15_000_000,
description:
@@ -469,7 +469,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 70,
crystalReward: 12_000_000,
crystalReward: 0,
currentHp: 4_000_000_000_000,
damagePerSecond: 50_000_000,
description:
@@ -487,7 +487,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 85,
crystalReward: 40_000_000,
crystalReward: 0,
currentHp: 15_000_000_000_000,
damagePerSecond: 150_000_000,
description:
@@ -505,7 +505,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 100,
crystalReward: 150_000_000,
crystalReward: 0,
currentHp: 50_000_000_000_000,
damagePerSecond: 500_000_000,
description:
@@ -524,7 +524,7 @@ export const defaultBosses: Array<Boss> = [
// ── Infernal Court ────────────────────────────────────────────────────────
{
bountyRunestones: 55,
crystalReward: 350_000_000,
crystalReward: 0,
currentHp: 120_000_000_000_000,
damagePerSecond: 800_000_000,
description:
@@ -542,7 +542,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 70,
crystalReward: 1_000_000_000,
crystalReward: 0,
currentHp: 500_000_000_000_000,
damagePerSecond: 2_500_000_000,
description:
@@ -560,7 +560,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 90,
crystalReward: 3_000_000_000,
crystalReward: 0,
currentHp: 2_000_000_000_000_000,
damagePerSecond: 8_000_000_000,
description:
@@ -578,7 +578,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 110,
crystalReward: 10_000_000_000,
crystalReward: 0,
currentHp: 6_000_000_000_000_000,
damagePerSecond: 25_000_000_000,
description:
@@ -596,7 +596,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 135,
crystalReward: 30_000_000_000,
crystalReward: 0,
currentHp: 8_000_000_000_000_000,
damagePerSecond: 80_000_000_000,
description:
@@ -615,7 +615,7 @@ export const defaultBosses: Array<Boss> = [
// ── Crystalline Spire ─────────────────────────────────────────────────────
{
bountyRunestones: 70,
crystalReward: 8e10,
crystalReward: 0,
currentHp: 2e16,
damagePerSecond: 120_000_000_000,
description:
@@ -633,7 +633,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 90,
crystalReward: 3e11,
crystalReward: 0,
currentHp: 8e16,
damagePerSecond: 4e11,
description:
@@ -651,7 +651,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 115,
crystalReward: 1e12,
crystalReward: 0,
currentHp: 3e17,
damagePerSecond: 1.2e12,
description:
@@ -669,7 +669,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 140,
crystalReward: 4e12,
crystalReward: 0,
currentHp: 1e18,
damagePerSecond: 4e12,
description:
@@ -687,7 +687,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 175,
crystalReward: 1.5e13,
crystalReward: 0,
currentHp: 4e18,
damagePerSecond: 1.5e13,
description:
@@ -706,7 +706,7 @@ export const defaultBosses: Array<Boss> = [
// ── Void Sanctum ──────────────────────────────────────────────────────────
{
bountyRunestones: 90,
crystalReward: 4e13,
crystalReward: 0,
currentHp: 1e19,
damagePerSecond: 4e13,
description:
@@ -724,7 +724,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 115,
crystalReward: 1.5e14,
crystalReward: 0,
currentHp: 5e19,
damagePerSecond: 1.5e14,
description:
@@ -742,7 +742,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 145,
crystalReward: 5e14,
crystalReward: 0,
currentHp: 2e20,
damagePerSecond: 5e14,
description:
@@ -760,7 +760,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 180,
crystalReward: 2e15,
crystalReward: 0,
currentHp: 8e20,
damagePerSecond: 2e15,
description:
@@ -778,7 +778,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 225,
crystalReward: 8e15,
crystalReward: 0,
currentHp: 3e21,
damagePerSecond: 8e15,
description:
@@ -797,7 +797,7 @@ export const defaultBosses: Array<Boss> = [
// ── Eternal Throne ────────────────────────────────────────────────────────
{
bountyRunestones: 115,
crystalReward: 2e16,
crystalReward: 0,
currentHp: 1e22,
damagePerSecond: 2e16,
description:
@@ -815,7 +815,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 150,
crystalReward: 8e16,
crystalReward: 0,
currentHp: 5e22,
damagePerSecond: 8e16,
description:
@@ -833,7 +833,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 190,
crystalReward: 3e17,
crystalReward: 0,
currentHp: 2e23,
damagePerSecond: 3e17,
description:
@@ -851,7 +851,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 235,
crystalReward: 1.2e18,
crystalReward: 0,
currentHp: 8e23,
damagePerSecond: 1.2e18,
description:
@@ -869,7 +869,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 295,
crystalReward: 5e18,
crystalReward: 0,
currentHp: 3e24,
damagePerSecond: 5e18,
description:
@@ -888,7 +888,7 @@ export const defaultBosses: Array<Boss> = [
// ── Primordial Chaos ──────────────────────────────────────────────────────
{
bountyRunestones: 150,
crystalReward: 2e20,
crystalReward: 0,
currentHp: 1e26,
damagePerSecond: 2e20,
description:
@@ -906,7 +906,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 200,
crystalReward: 8e21,
crystalReward: 0,
currentHp: 5e27,
damagePerSecond: 8e21,
description:
@@ -924,7 +924,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 265,
crystalReward: 4e23,
crystalReward: 0,
currentHp: 2e29,
damagePerSecond: 4e23,
description:
@@ -942,7 +942,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 350,
crystalReward: 2e25,
crystalReward: 0,
currentHp: 8e30,
damagePerSecond: 2e25,
description:
@@ -961,7 +961,7 @@ export const defaultBosses: Array<Boss> = [
// ── Infinite Expanse ──────────────────────────────────────────────────────
{
bountyRunestones: 200,
crystalReward: 8e27,
crystalReward: 0,
currentHp: 3e33,
damagePerSecond: 8e27,
description:
@@ -979,7 +979,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 265,
crystalReward: 3e31,
crystalReward: 0,
currentHp: 2e35,
damagePerSecond: 3e31,
description:
@@ -997,7 +997,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 350,
crystalReward: 1e35,
crystalReward: 0,
currentHp: 5e37,
damagePerSecond: 1e35,
description:
@@ -1015,7 +1015,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 465,
crystalReward: 5e38,
crystalReward: 0,
currentHp: 3e39,
damagePerSecond: 5e38,
description:
@@ -1034,7 +1034,7 @@ export const defaultBosses: Array<Boss> = [
// ── Reality Forge ─────────────────────────────────────────────────────────
{
bountyRunestones: 265,
crystalReward: 2e42,
crystalReward: 0,
currentHp: 8e47,
damagePerSecond: 2e42,
description:
@@ -1052,7 +1052,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 350,
crystalReward: 1e47,
crystalReward: 0,
currentHp: 4e52,
damagePerSecond: 1e47,
description:
@@ -1070,7 +1070,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 465,
crystalReward: 6e51,
crystalReward: 0,
currentHp: 2e57,
damagePerSecond: 6e51,
description:
@@ -1088,7 +1088,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 615,
crystalReward: 2e56,
crystalReward: 0,
currentHp: 8e61,
damagePerSecond: 2e56,
description:
@@ -1107,7 +1107,7 @@ export const defaultBosses: Array<Boss> = [
// ── Cosmic Maelstrom ──────────────────────────────────────────────────────
{
bountyRunestones: 350,
crystalReward: 1e60,
crystalReward: 0,
currentHp: 4e65,
damagePerSecond: 1e60,
description:
@@ -1125,7 +1125,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 465,
crystalReward: 6e65,
crystalReward: 0,
currentHp: 2e71,
damagePerSecond: 6e65,
description:
@@ -1143,7 +1143,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 615,
crystalReward: 3e71,
crystalReward: 0,
currentHp: 1e77,
damagePerSecond: 3e71,
description:
@@ -1161,7 +1161,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 815,
crystalReward: 1e77,
crystalReward: 0,
currentHp: 5e82,
damagePerSecond: 1e77,
description:
@@ -1180,7 +1180,7 @@ export const defaultBosses: Array<Boss> = [
// ── Primeval Sanctum ──────────────────────────────────────────────────────
{
bountyRunestones: 465,
crystalReward: 5e82,
crystalReward: 0,
currentHp: 2e88,
damagePerSecond: 5e82,
description:
@@ -1198,7 +1198,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 615,
crystalReward: 3e89,
crystalReward: 0,
currentHp: 1e95,
damagePerSecond: 3e89,
description:
@@ -1216,7 +1216,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 815,
crystalReward: 2e96,
crystalReward: 0,
currentHp: 8e101,
damagePerSecond: 2e96,
description:
@@ -1234,7 +1234,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 1080,
crystalReward: 1e103,
crystalReward: 0,
currentHp: 5e108,
damagePerSecond: 1e103,
description:
@@ -1253,7 +1253,7 @@ export const defaultBosses: Array<Boss> = [
// ── The Absolute ──────────────────────────────────────────────────────────
{
bountyRunestones: 615,
crystalReward: 5e110,
crystalReward: 0,
currentHp: 2e116,
damagePerSecond: 5e110,
description:
@@ -1271,7 +1271,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 815,
crystalReward: 3e119,
crystalReward: 0,
currentHp: 1e125,
damagePerSecond: 3e119,
description:
@@ -1289,7 +1289,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 1080,
crystalReward: 1e129,
crystalReward: 0,
currentHp: 5e134,
damagePerSecond: 1e129,
description:
@@ -1307,7 +1307,7 @@ export const defaultBosses: Array<Boss> = [
},
{
bountyRunestones: 1430,
crystalReward: 5e139,
crystalReward: 0,
currentHp: 2e145,
damagePerSecond: 5e139,
description:
+19 -14
View File
@@ -11,7 +11,6 @@
/* eslint-disable max-lines -- Engine file necessarily exceeds line limit */
/* eslint-disable import/group-exports -- Exports appear alongside their definitions for readability */
/* eslint-disable import/exports-last -- Exports appear alongside their definitions for readability */
/* eslint-disable unicorn/no-array-reduce -- reduce is the most readable approach for multiplier chains */
/* eslint-disable max-nested-callbacks -- Tick engine requires nested array operations for game logic */
import {
type Achievement,
@@ -818,24 +817,30 @@ export const applyTick = (
zones: updatedZones,
};
// Check achievements and apply crystal rewards for newly unlocked ones
// Check achievements and apply crystal and runestone rewards for newly unlocked ones
const updatedAchievements = checkAchievements(partialState);
const crystalsFromAchievements = updatedAchievements.reduce(
(sum, achievement, index) => {
const wasLocked = state.achievements[index]?.unlockedAt === null;
const isNowUnlocked = achievement.unlockedAt !== null;
if (wasLocked && isNowUnlocked) {
return sum + (achievement.reward?.crystals ?? 0);
}
return sum;
},
0,
);
let crystalsFromAchievements = 0;
let runestonesFromAchievements = 0;
for (const [ index, achievement ] of updatedAchievements.entries()) {
const wasLocked = state.achievements[index]?.unlockedAt === null;
const isNowUnlocked = achievement.unlockedAt !== null;
if (wasLocked && isNowUnlocked) {
crystalsFromAchievements
= crystalsFromAchievements + (achievement.reward?.crystals ?? 0);
runestonesFromAchievements
= runestonesFromAchievements + (achievement.reward?.runestones ?? 0);
}
}
return {
...partialState,
achievements: updatedAchievements,
resources: {
prestige: {
...partialState.prestige,
runestones:
partialState.prestige.runestones + runestonesFromAchievements,
},
resources: {
...partialState.resources,
crystals: capResource(
partialState.resources.crystals + crystalsFromAchievements,
+2 -1
View File
@@ -20,7 +20,8 @@ interface AchievementCondition {
}
interface AchievementReward {
crystals?: number;
crystals?: number;
runestones?: number;
}
interface Achievement {