generated from nhcarrigan/template
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
87686a310f
|
|||
|
19f5f5e54f
|
|||
|
7d1126e8ad
|
|||
|
ec0763819e
|
|||
|
4a9ecbf706
|
|||
|
96868c4143
|
|||
|
48477ee286
|
|||
|
b3d257048f
|
|||
|
3735cff23f
|
|||
|
a09280470e
|
|||
|
48120e0789
|
|||
|
0542402b4d
|
|||
|
689133d05d
|
|||
|
8a332dc9ce
|
|||
|
56d963dc90
|
|||
|
77c7ee02a6
|
|||
|
d1559c327f
|
|||
|
4c297f1ce1
|
|||
|
b6e218167d
|
|||
|
0609cc7584
|
|||
|
7c390f45b5
|
|||
|
7ecc655484
|
|||
|
4b3a856ef9
|
|||
|
d84725921a
|
|||
|
e4808680ed
|
|||
|
f001acc382
|
|||
|
8a38d02e69
|
|||
|
eed61db410
|
|||
|
0ae6aa12b2
|
|||
|
0d6d05e50b
|
|||
|
74dd3bf463
|
|||
|
959b86fa8b
|
@@ -26,7 +26,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
||||
combatPower: 3,
|
||||
count: 0,
|
||||
essencePerSecond: 0,
|
||||
goldPerSecond: 0.5,
|
||||
goldPerSecond: 0.7,
|
||||
id: "militia",
|
||||
level: 2,
|
||||
name: "Militia",
|
||||
@@ -129,7 +129,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
||||
unlocked: false,
|
||||
},
|
||||
{
|
||||
baseCost: 2_600_000_000,
|
||||
baseCost: 2_850_000_000,
|
||||
class: "mage",
|
||||
combatPower: 13_000,
|
||||
count: 0,
|
||||
@@ -141,7 +141,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
||||
unlocked: false,
|
||||
},
|
||||
{
|
||||
baseCost: 11_000_000_000,
|
||||
baseCost: 13_500_000_000,
|
||||
class: "rogue",
|
||||
combatPower: 28_000,
|
||||
count: 0,
|
||||
@@ -153,7 +153,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
||||
unlocked: false,
|
||||
},
|
||||
{
|
||||
baseCost: 47_000_000_000,
|
||||
baseCost: 64_000_000_000,
|
||||
class: "paladin",
|
||||
combatPower: 60_000,
|
||||
count: 0,
|
||||
@@ -165,7 +165,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
||||
unlocked: false,
|
||||
},
|
||||
{
|
||||
baseCost: 200_000_000_000,
|
||||
baseCost: 300_000_000_000,
|
||||
class: "rogue",
|
||||
combatPower: 130_000,
|
||||
count: 0,
|
||||
@@ -177,7 +177,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
||||
unlocked: false,
|
||||
},
|
||||
{
|
||||
baseCost: 1_400_000_000_000,
|
||||
baseCost: 1_800_000_000_000,
|
||||
class: "paladin",
|
||||
combatPower: 400_000,
|
||||
count: 0,
|
||||
|
||||
+70
-70
@@ -122,7 +122,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
// ── Shadow Marshes ────────────────────────────────────────────────────────
|
||||
{
|
||||
bountyRunestones: 20,
|
||||
crystalReward: 700,
|
||||
crystalReward: 1500,
|
||||
currentHp: 6_000_000,
|
||||
damagePerSecond: 1200,
|
||||
description:
|
||||
@@ -140,7 +140,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
},
|
||||
{
|
||||
bountyRunestones: 25,
|
||||
crystalReward: 1500,
|
||||
crystalReward: 3000,
|
||||
currentHp: 12_000_000,
|
||||
damagePerSecond: 2400,
|
||||
description:
|
||||
@@ -158,7 +158,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
},
|
||||
{
|
||||
bountyRunestones: 30,
|
||||
crystalReward: 3000,
|
||||
crystalReward: 6000,
|
||||
currentHp: 20_000_000,
|
||||
damagePerSecond: 4000,
|
||||
description:
|
||||
@@ -226,7 +226,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
name: "The Void Titan",
|
||||
prestigeRequirement: 0,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "dark_templar_1" ],
|
||||
zoneId: "frozen_peaks",
|
||||
},
|
||||
// ── Volcanic Depths ───────────────────────────────────────────────────────
|
||||
@@ -353,7 +353,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "seraph_guardian",
|
||||
maxHp: 500_000_000,
|
||||
name: "The Seraph Guardian",
|
||||
prestigeRequirement: 6,
|
||||
prestigeRequirement: 1,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "click_4" ],
|
||||
zoneId: "celestial_reaches",
|
||||
@@ -371,7 +371,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "fallen_archangel",
|
||||
maxHp: 2_000_000_000,
|
||||
name: "The Fallen Archangel",
|
||||
prestigeRequirement: 7,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "celestial_reaches",
|
||||
@@ -389,7 +389,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "divine_judge",
|
||||
maxHp: 8_000_000_000,
|
||||
name: "The Divine Judge",
|
||||
prestigeRequirement: 8,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "divine_covenant" ],
|
||||
zoneId: "celestial_reaches",
|
||||
@@ -407,7 +407,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "celestial_titan",
|
||||
maxHp: 30_000_000_000,
|
||||
name: "The Celestial Titan",
|
||||
prestigeRequirement: 9,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "celestial_reaches",
|
||||
@@ -425,7 +425,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_first_light",
|
||||
maxHp: 100_000_000_000,
|
||||
name: "The First Light",
|
||||
prestigeRequirement: 10,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "celestial_reaches",
|
||||
@@ -444,7 +444,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "depth_leviathan",
|
||||
maxHp: 250_000_000_000,
|
||||
name: "The Depth Leviathan",
|
||||
prestigeRequirement: 9,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -462,7 +462,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "kraken_elder",
|
||||
maxHp: 1_000_000_000_000,
|
||||
name: "The Elder Kraken",
|
||||
prestigeRequirement: 10,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "abyssal_pact" ],
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -480,7 +480,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "abyssal_colossus",
|
||||
maxHp: 4_000_000_000_000,
|
||||
name: "The Abyssal Colossus",
|
||||
prestigeRequirement: 11,
|
||||
prestigeRequirement: 2,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -498,7 +498,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_deep_one",
|
||||
maxHp: 15_000_000_000_000,
|
||||
name: "The Deep One",
|
||||
prestigeRequirement: 12,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "global_4" ],
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -516,7 +516,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "elder_abomination",
|
||||
maxHp: 50_000_000_000_000,
|
||||
name: "The Elder Abomination",
|
||||
prestigeRequirement: 13,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -535,7 +535,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "demon_prince",
|
||||
maxHp: 120_000_000_000_000,
|
||||
name: "The Demon Prince",
|
||||
prestigeRequirement: 12,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "infernal_court",
|
||||
@@ -553,7 +553,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "hellfire_titan",
|
||||
maxHp: 500_000_000_000_000,
|
||||
name: "The Hellfire Titan",
|
||||
prestigeRequirement: 13,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "celestial_mandate" ],
|
||||
zoneId: "infernal_court",
|
||||
@@ -571,7 +571,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "lord_of_sin",
|
||||
maxHp: 2_000_000_000_000_000,
|
||||
name: "The Lord of Sin",
|
||||
prestigeRequirement: 14,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "infernal_court",
|
||||
@@ -589,7 +589,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "infernal_sovereign",
|
||||
maxHp: 6_000_000_000_000_000,
|
||||
name: "The Infernal Sovereign",
|
||||
prestigeRequirement: 15,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "click_5" ],
|
||||
zoneId: "infernal_court",
|
||||
@@ -607,7 +607,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_fallen",
|
||||
maxHp: 8_000_000_000_000_000,
|
||||
name: "The Fallen",
|
||||
prestigeRequirement: 16,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "infernal_court",
|
||||
@@ -626,9 +626,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "prism_golem",
|
||||
maxHp: 2e16,
|
||||
name: "The Prism Golem",
|
||||
prestigeRequirement: 15,
|
||||
prestigeRequirement: 3,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "crystal_sage_1" ],
|
||||
zoneId: "crystalline_spire",
|
||||
},
|
||||
{
|
||||
@@ -644,7 +644,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "crystal_drake",
|
||||
maxHp: 8e16,
|
||||
name: "The Crystal Drake",
|
||||
prestigeRequirement: 16,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "void_ascendancy" ],
|
||||
zoneId: "crystalline_spire",
|
||||
@@ -662,9 +662,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_faceted",
|
||||
maxHp: 3e17,
|
||||
name: "The Faceted",
|
||||
prestigeRequirement: 17,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "void_sentinel_1" ],
|
||||
zoneId: "crystalline_spire",
|
||||
},
|
||||
{
|
||||
@@ -680,9 +680,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "diamond_colossus",
|
||||
maxHp: 1e18,
|
||||
name: "The Diamond Colossus",
|
||||
prestigeRequirement: 18,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "eternal_champion_1" ],
|
||||
zoneId: "crystalline_spire",
|
||||
},
|
||||
{
|
||||
@@ -698,9 +698,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "crystal_sovereign",
|
||||
maxHp: 4e18,
|
||||
name: "The Crystal Sovereign",
|
||||
prestigeRequirement: 19,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "cosmos_knight_1" ],
|
||||
zoneId: "crystalline_spire",
|
||||
},
|
||||
// ── Void Sanctum ──────────────────────────────────────────────────────────
|
||||
@@ -717,9 +717,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "void_herald",
|
||||
maxHp: 1e19,
|
||||
name: "The Void Herald",
|
||||
prestigeRequirement: 18,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "seraph_knight_1" ],
|
||||
zoneId: "void_sanctum",
|
||||
},
|
||||
{
|
||||
@@ -735,7 +735,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "eternal_shade",
|
||||
maxHp: 5e19,
|
||||
name: "The Eternal Shade",
|
||||
prestigeRequirement: 19,
|
||||
prestigeRequirement: 4,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "divine_harmony" ],
|
||||
zoneId: "void_sanctum",
|
||||
@@ -753,9 +753,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_unmaker",
|
||||
maxHp: 2e20,
|
||||
name: "The Unmaker",
|
||||
prestigeRequirement: 20,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "abyss_diver_1" ],
|
||||
zoneId: "void_sanctum",
|
||||
},
|
||||
{
|
||||
@@ -771,7 +771,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "void_progenitor",
|
||||
maxHp: 8e20,
|
||||
name: "The Void Progenitor",
|
||||
prestigeRequirement: 21,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "void_sanctum",
|
||||
@@ -789,9 +789,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "void_emperor",
|
||||
maxHp: 3e21,
|
||||
name: "The Void Emperor",
|
||||
prestigeRequirement: 22,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "infernal_warden_1" ],
|
||||
zoneId: "void_sanctum",
|
||||
},
|
||||
// ── Eternal Throne ────────────────────────────────────────────────────────
|
||||
@@ -808,9 +808,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "throne_warden",
|
||||
maxHp: 1e22,
|
||||
name: "The Throne Warden",
|
||||
prestigeRequirement: 21,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "infinity_ranger_1" ],
|
||||
zoneId: "eternal_throne",
|
||||
},
|
||||
{
|
||||
@@ -826,7 +826,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "eternal_knight",
|
||||
maxHp: 5e22,
|
||||
name: "The Eternal Knight",
|
||||
prestigeRequirement: 22,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "infernal_fury" ],
|
||||
zoneId: "eternal_throne",
|
||||
@@ -844,9 +844,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_undying",
|
||||
maxHp: 2e23,
|
||||
name: "The Undying",
|
||||
prestigeRequirement: 23,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "reality_warden_1" ],
|
||||
zoneId: "eternal_throne",
|
||||
},
|
||||
{
|
||||
@@ -862,7 +862,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "apex_sovereign",
|
||||
maxHp: 8e23,
|
||||
name: "The Apex Sovereign",
|
||||
prestigeRequirement: 24,
|
||||
prestigeRequirement: 5,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "eternal_throne",
|
||||
@@ -880,7 +880,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_apex",
|
||||
maxHp: 3e24,
|
||||
name: "The Apex",
|
||||
prestigeRequirement: 25,
|
||||
prestigeRequirement: 6,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "eternal_throne",
|
||||
@@ -899,7 +899,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "chaos_wyrm",
|
||||
maxHp: 1e26,
|
||||
name: "The Chaos Wyrm",
|
||||
prestigeRequirement: 26,
|
||||
prestigeRequirement: 6,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "primordial_chaos",
|
||||
@@ -917,7 +917,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "creation_engine",
|
||||
maxHp: 5e27,
|
||||
name: "The Creation Engine",
|
||||
prestigeRequirement: 27,
|
||||
prestigeRequirement: 6,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "aether_weaver_1" ],
|
||||
zoneId: "primordial_chaos",
|
||||
@@ -935,7 +935,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "entropy_avatar",
|
||||
maxHp: 2e29,
|
||||
name: "The Entropy Avatar",
|
||||
prestigeRequirement: 29,
|
||||
prestigeRequirement: 7,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "primordial_chaos",
|
||||
@@ -953,7 +953,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "primordial_titan",
|
||||
maxHp: 8e30,
|
||||
name: "The Primordial Titan",
|
||||
prestigeRequirement: 31,
|
||||
prestigeRequirement: 7,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "primordial_chaos",
|
||||
@@ -972,7 +972,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "expanse_drifter",
|
||||
maxHp: 3e33,
|
||||
name: "The Expanse Drifter",
|
||||
prestigeRequirement: 33,
|
||||
prestigeRequirement: 8,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "titan_warrior_1" ],
|
||||
zoneId: "infinite_expanse",
|
||||
@@ -990,9 +990,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "horizon_beast",
|
||||
maxHp: 1e37,
|
||||
name: "The Horizon Beast",
|
||||
prestigeRequirement: 35,
|
||||
prestigeRequirement: 8,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "oblivion_paladin_1" ],
|
||||
zoneId: "infinite_expanse",
|
||||
},
|
||||
{
|
||||
@@ -1008,7 +1008,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "infinity_construct",
|
||||
maxHp: 5e40,
|
||||
name: "The Infinity Construct",
|
||||
prestigeRequirement: 37,
|
||||
prestigeRequirement: 8,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "infinite_expanse",
|
||||
@@ -1026,7 +1026,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "expanse_sovereign",
|
||||
maxHp: 2e44,
|
||||
name: "The Expanse Sovereign",
|
||||
prestigeRequirement: 39,
|
||||
prestigeRequirement: 9,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "infinite_expanse",
|
||||
@@ -1045,7 +1045,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "forge_guardian",
|
||||
maxHp: 8e47,
|
||||
name: "The Forge Guardian",
|
||||
prestigeRequirement: 41,
|
||||
prestigeRequirement: 9,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "nexus_sage_1" ],
|
||||
zoneId: "reality_forge",
|
||||
@@ -1063,7 +1063,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "reality_shaper",
|
||||
maxHp: 4e52,
|
||||
name: "The Reality Shaper",
|
||||
prestigeRequirement: 44,
|
||||
prestigeRequirement: 10,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "reality_forge",
|
||||
@@ -1081,7 +1081,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "creation_prime",
|
||||
maxHp: 2e57,
|
||||
name: "The Creation Prime",
|
||||
prestigeRequirement: 47,
|
||||
prestigeRequirement: 11,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "reality_forge",
|
||||
@@ -1099,7 +1099,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "reality_architect",
|
||||
maxHp: 8e61,
|
||||
name: "The Reality Architect",
|
||||
prestigeRequirement: 49,
|
||||
prestigeRequirement: 11,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "reality_forge",
|
||||
@@ -1118,7 +1118,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "storm_colossus",
|
||||
maxHp: 4e65,
|
||||
name: "The Storm Colossus",
|
||||
prestigeRequirement: 51,
|
||||
prestigeRequirement: 12,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "cosmic_maelstrom",
|
||||
@@ -1136,7 +1136,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "force_prime",
|
||||
maxHp: 2e71,
|
||||
name: "The Force Prime",
|
||||
prestigeRequirement: 54,
|
||||
prestigeRequirement: 12,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "cosmic_maelstrom",
|
||||
@@ -1154,9 +1154,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "maelstrom_god",
|
||||
maxHp: 1e77,
|
||||
name: "The Maelstrom God",
|
||||
prestigeRequirement: 57,
|
||||
prestigeRequirement: 13,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "transcendent_rogue_1" ],
|
||||
zoneId: "cosmic_maelstrom",
|
||||
},
|
||||
{
|
||||
@@ -1172,7 +1172,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "cosmic_annihilator",
|
||||
maxHp: 5e82,
|
||||
name: "The Cosmic Annihilator",
|
||||
prestigeRequirement: 59,
|
||||
prestigeRequirement: 13,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "cosmic_maelstrom",
|
||||
@@ -1191,7 +1191,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "ancient_sentinel",
|
||||
maxHp: 2e88,
|
||||
name: "The Ancient Sentinel",
|
||||
prestigeRequirement: 61,
|
||||
prestigeRequirement: 14,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "astral_sovereign_1" ],
|
||||
zoneId: "primeval_sanctum",
|
||||
@@ -1209,7 +1209,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "time_elder",
|
||||
maxHp: 1e95,
|
||||
name: "The Time Elder",
|
||||
prestigeRequirement: 65,
|
||||
prestigeRequirement: 15,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "primeval_sanctum",
|
||||
@@ -1227,7 +1227,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "origin_beast",
|
||||
maxHp: 8e101,
|
||||
name: "The Origin Beast",
|
||||
prestigeRequirement: 69,
|
||||
prestigeRequirement: 16,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "primeval_sanctum",
|
||||
@@ -1245,7 +1245,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "primeval_god",
|
||||
maxHp: 5e108,
|
||||
name: "The Primeval God",
|
||||
prestigeRequirement: 74,
|
||||
prestigeRequirement: 17,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "primeval_sanctum",
|
||||
@@ -1264,7 +1264,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "absolute_herald",
|
||||
maxHp: 2e116,
|
||||
name: "The Absolute Herald",
|
||||
prestigeRequirement: 76,
|
||||
prestigeRequirement: 17,
|
||||
status: "locked",
|
||||
upgradeRewards: [ "primordial_mage_1" ],
|
||||
zoneId: "the_absolute",
|
||||
@@ -1282,7 +1282,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "void_convergence",
|
||||
maxHp: 1e125,
|
||||
name: "The Void Convergence",
|
||||
prestigeRequirement: 79,
|
||||
prestigeRequirement: 18,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "the_absolute",
|
||||
@@ -1300,9 +1300,9 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "eternal_end",
|
||||
maxHp: 5e134,
|
||||
name: "The Eternal End",
|
||||
prestigeRequirement: 83,
|
||||
prestigeRequirement: 19,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
upgradeRewards: [ "omniversal_champion_1" ],
|
||||
zoneId: "the_absolute",
|
||||
},
|
||||
{
|
||||
@@ -1318,7 +1318,7 @@ export const defaultBosses: Array<Boss> = [
|
||||
id: "the_absolute_one",
|
||||
maxHp: 2e145,
|
||||
name: "The Absolute One",
|
||||
prestigeRequirement: 88,
|
||||
prestigeRequirement: 20,
|
||||
status: "locked",
|
||||
upgradeRewards: [],
|
||||
zoneId: "the_absolute",
|
||||
|
||||
@@ -269,7 +269,7 @@ export const defaultEquipment: Array<Equipment> = [
|
||||
type: "trinket",
|
||||
},
|
||||
{
|
||||
bonus: { clickMultiplier: 1.55, goldMultiplier: 1.1 },
|
||||
bonus: { clickMultiplier: 1.65, goldMultiplier: 1.2 },
|
||||
description:
|
||||
"A fragment of the Mud Kraken's crystallised essence. Focuses raw power into devastating strikes.",
|
||||
equipped: false,
|
||||
@@ -305,9 +305,9 @@ export const defaultEquipment: Array<Equipment> = [
|
||||
type: "trinket",
|
||||
},
|
||||
{
|
||||
bonus: { clickMultiplier: 2.25, goldMultiplier: 1.25 },
|
||||
bonus: { clickMultiplier: 2.5, goldMultiplier: 1.4 },
|
||||
description:
|
||||
"The legendary stone that grants mastery over gold and combat alike.",
|
||||
"The legendary stone that transmutes effort into wealth — every action fills the coffers.",
|
||||
equipped: false,
|
||||
id: "philosophers_stone",
|
||||
name: "Philosopher's Stone",
|
||||
@@ -697,7 +697,7 @@ export const defaultEquipment: Array<Equipment> = [
|
||||
},
|
||||
// ── Purchasable endgame sinks ─────────────────────────────────────────────
|
||||
{
|
||||
bonus: { clickMultiplier: 3 },
|
||||
bonus: { clickMultiplier: 4.25 },
|
||||
cost: { crystals: 0, essence: 20_000_000, gold: 0 },
|
||||
description:
|
||||
"A lens of compressed celestial light that sharpens every strike with divine precision.",
|
||||
@@ -721,7 +721,7 @@ export const defaultEquipment: Array<Equipment> = [
|
||||
type: "armour",
|
||||
},
|
||||
{
|
||||
bonus: { combatMultiplier: 7 },
|
||||
bonus: { combatMultiplier: 10.5 },
|
||||
cost: { crystals: 0, essence: 100_000_000, gold: 0 },
|
||||
description:
|
||||
"A weapon that channels void energy — the absence of resistance makes every strike devastating.",
|
||||
@@ -745,7 +745,7 @@ export const defaultEquipment: Array<Equipment> = [
|
||||
type: "trinket",
|
||||
},
|
||||
{
|
||||
bonus: { goldMultiplier: 4.75 },
|
||||
bonus: { goldMultiplier: 7.5 },
|
||||
cost: { crystals: 20_000_000, essence: 0, gold: 0 },
|
||||
description:
|
||||
"Armour structured around a crystalline lattice of optimal income calculations. Every gold piece finds you faster.",
|
||||
|
||||
+64
-27
@@ -34,6 +34,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 2000, type: "gold" },
|
||||
{ amount: 5, type: "essence" },
|
||||
{ targetId: "peasant_1", type: "upgrade" },
|
||||
{ targetId: "apprentice_1", type: "upgrade" },
|
||||
{ targetId: "apprentice", type: "adventurer" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -50,6 +51,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 10, type: "crystals" },
|
||||
{ targetId: "global_1", type: "upgrade" },
|
||||
{ targetId: "militia_1", type: "upgrade" },
|
||||
{ targetId: "scout", type: "adventurer" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -82,7 +84,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 15_000, type: "gold" },
|
||||
{ amount: 20, type: "essence" },
|
||||
{ targetId: "militia_1", type: "upgrade" },
|
||||
{ targetId: "acolyte_1", type: "upgrade" },
|
||||
{ targetId: "ranger", type: "adventurer" },
|
||||
],
|
||||
@@ -117,7 +118,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 300, type: "essence" },
|
||||
{ amount: 30, type: "crystals" },
|
||||
{ targetId: "apprentice_1", type: "upgrade" },
|
||||
{ targetId: "archmage", type: "adventurer" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -153,6 +153,23 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 5_000_000, type: "gold" },
|
||||
{ amount: 100, type: "crystals" },
|
||||
{ targetId: "global_3", type: "upgrade" },
|
||||
{ targetId: "knight_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "frozen_peaks",
|
||||
},
|
||||
{
|
||||
combatPowerRequired: 200_000,
|
||||
description:
|
||||
"A tomb sealed within a glacier for millennia. The soldiers interred here died guarding something that no longer exists — but their treasures remain.",
|
||||
durationSeconds: 150 * 60,
|
||||
id: "glacier_tomb",
|
||||
name: "The Glacier Tomb",
|
||||
prerequisiteIds: [ "frozen_wastes" ],
|
||||
rewards: [
|
||||
{ amount: 10_000_000, type: "gold" },
|
||||
{ amount: 3000, type: "essence" },
|
||||
{ targetId: "peasant_2", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "frozen_peaks",
|
||||
@@ -164,7 +181,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
durationSeconds: 3 * 60 * 60,
|
||||
id: "ice_caves",
|
||||
name: "The Ice Caves",
|
||||
prerequisiteIds: [ "frozen_wastes" ],
|
||||
prerequisiteIds: [ "glacier_tomb" ],
|
||||
rewards: [
|
||||
{ amount: 5000, type: "essence" },
|
||||
{ amount: 200, type: "crystals" },
|
||||
@@ -188,9 +205,25 @@ export const defaultQuests: Array<Quest> = [
|
||||
status: "locked",
|
||||
zoneId: "frozen_peaks",
|
||||
},
|
||||
{
|
||||
combatPowerRequired: 3_000_000,
|
||||
description:
|
||||
"Deep in the peaks lies the throne room of an ancient frost king, long dead, whose dominion over cold and storm was absolute. His crown still waits.",
|
||||
durationSeconds: 7 * 60 * 60,
|
||||
id: "frozen_throne",
|
||||
name: "The Frozen Throne",
|
||||
prerequisiteIds: [ "storm_citadel" ],
|
||||
rewards: [
|
||||
{ amount: 60_000_000, type: "gold" },
|
||||
{ amount: 25_000, type: "essence" },
|
||||
{ amount: 400, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "frozen_peaks",
|
||||
},
|
||||
// ── Shadow Marshes ────────────────────────────────────────────────────────
|
||||
{
|
||||
combatPowerRequired: 5_000_000,
|
||||
combatPowerRequired: 2_000_000,
|
||||
description:
|
||||
"A cursed lake shrouded in permanent twilight. Strange energies pulse beneath its surface.",
|
||||
durationSeconds: 45 * 60,
|
||||
@@ -198,7 +231,10 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "The Shadow Mere",
|
||||
prerequisiteIds: [],
|
||||
rewards: [
|
||||
{ amount: 150, type: "essence" },
|
||||
{ amount: 5_000_000, type: "gold" },
|
||||
{ amount: 5000, type: "essence" },
|
||||
{ amount: 150, type: "crystals" },
|
||||
{ targetId: "peasant_3", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "shadow_marshes",
|
||||
@@ -212,7 +248,9 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "The Witch Coven",
|
||||
prerequisiteIds: [ "shadow_mere" ],
|
||||
rewards: [
|
||||
{ amount: 500, type: "essence" },
|
||||
{ amount: 20_000_000, type: "gold" },
|
||||
{ amount: 20_000, type: "essence" },
|
||||
{ amount: 500, type: "crystals" },
|
||||
{ targetId: "shadow_assassin", type: "adventurer" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -230,8 +268,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 2_000_000, type: "gold" },
|
||||
{ amount: 1500, type: "essence" },
|
||||
{ amount: 75, type: "crystals" },
|
||||
{ targetId: "knight_1", type: "upgrade" },
|
||||
{ targetId: "peasant_2", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "shadow_marshes",
|
||||
@@ -245,9 +281,9 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "The Plague Ruins",
|
||||
prerequisiteIds: [ "sunken_temple" ],
|
||||
rewards: [
|
||||
{ amount: 8_000_000, type: "gold" },
|
||||
{ amount: 2000, type: "essence" },
|
||||
{ amount: 150, type: "crystals" },
|
||||
{ amount: 100_000_000, type: "gold" },
|
||||
{ amount: 30_000, type: "essence" },
|
||||
{ amount: 500, type: "crystals" },
|
||||
{ targetId: "dark_templar", type: "adventurer" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -282,7 +318,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 40_000_000, type: "gold" },
|
||||
{ amount: 12_000, type: "essence" },
|
||||
{ amount: 300, type: "crystals" },
|
||||
{ targetId: "peasant_3", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "volcanic_depths",
|
||||
@@ -329,8 +364,9 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "Void Rift",
|
||||
prerequisiteIds: [],
|
||||
rewards: [
|
||||
{ amount: 500, type: "crystals" },
|
||||
{ amount: 5000, type: "essence" },
|
||||
{ amount: 2_000_000_000, type: "gold" },
|
||||
{ amount: 300_000, type: "essence" },
|
||||
{ amount: 1000, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "astral_void",
|
||||
@@ -344,9 +380,9 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "The Star Graveyard",
|
||||
prerequisiteIds: [ "void_rift" ],
|
||||
rewards: [
|
||||
{ amount: 1_000_000_000, type: "gold" },
|
||||
{ amount: 100_000, type: "essence" },
|
||||
{ amount: 1000, type: "crystals" },
|
||||
{ amount: 8_000_000_000, type: "gold" },
|
||||
{ amount: 800_000, type: "essence" },
|
||||
{ amount: 3000, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "astral_void",
|
||||
@@ -360,8 +396,9 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "Between Worlds",
|
||||
prerequisiteIds: [ "star_graveyard" ],
|
||||
rewards: [
|
||||
{ amount: 250_000, type: "essence" },
|
||||
{ amount: 2000, type: "crystals" },
|
||||
{ amount: 25_000_000_000, type: "gold" },
|
||||
{ amount: 2_000_000, type: "essence" },
|
||||
{ amount: 8000, type: "crystals" },
|
||||
{ targetId: "divine_champion", type: "adventurer" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -376,9 +413,9 @@ export const defaultQuests: Array<Quest> = [
|
||||
name: "The End of All Things",
|
||||
prerequisiteIds: [ "between_worlds" ],
|
||||
rewards: [
|
||||
{ amount: 10_000_000_000, type: "gold" },
|
||||
{ amount: 1_000_000, type: "essence" },
|
||||
{ amount: 10_000, type: "crystals" },
|
||||
{ amount: 80_000_000_000, type: "gold" },
|
||||
{ amount: 5_000_000, type: "essence" },
|
||||
{ amount: 20_000, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "astral_void",
|
||||
@@ -1148,6 +1185,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 8e39, type: "gold" },
|
||||
{ amount: 2.5e36, type: "essence" },
|
||||
{ amount: 5e32, type: "crystals" },
|
||||
{ targetId: "cosmos_knight_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "infinite_expanse",
|
||||
@@ -1230,7 +1268,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 2.5e49, type: "gold" },
|
||||
{ amount: 8e45, type: "essence" },
|
||||
{ amount: 5e41, type: "crystals" },
|
||||
{ targetId: "cosmos_knight_1", type: "upgrade" },
|
||||
{ targetId: "primordial_mage_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "reality_forge",
|
||||
@@ -1263,6 +1301,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 6e52, type: "gold" },
|
||||
{ amount: 2e49, type: "essence" },
|
||||
{ amount: 1.2e45, type: "crystals" },
|
||||
{ targetId: "astral_sovereign_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "reality_forge",
|
||||
@@ -1329,7 +1368,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 4e63, type: "gold" },
|
||||
{ amount: 1.2e60, type: "essence" },
|
||||
{ amount: 7e55, type: "crystals" },
|
||||
{ targetId: "astral_sovereign_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "cosmic_maelstrom",
|
||||
@@ -1346,6 +1384,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 2e66, type: "gold" },
|
||||
{ amount: 6e62, type: "essence" },
|
||||
{ amount: 3.5e58, type: "crystals" },
|
||||
{ targetId: "reality_warden_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "cosmic_maelstrom",
|
||||
@@ -1362,6 +1401,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 1e69, type: "gold" },
|
||||
{ amount: 3e65, type: "essence" },
|
||||
{ amount: 1.8e61, type: "crystals" },
|
||||
{ targetId: "infinity_ranger_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "cosmic_maelstrom",
|
||||
@@ -1428,7 +1468,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 6e83, type: "gold" },
|
||||
{ amount: 1.8e80, type: "essence" },
|
||||
{ amount: 1e76, type: "crystals" },
|
||||
{ targetId: "primordial_mage_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "primeval_sanctum",
|
||||
@@ -1510,7 +1549,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 2e108, type: "gold" },
|
||||
{ amount: 6e104, type: "essence" },
|
||||
{ amount: 3e100, type: "crystals" },
|
||||
{ targetId: "reality_warden_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "the_absolute",
|
||||
@@ -1543,7 +1581,6 @@ export const defaultQuests: Array<Quest> = [
|
||||
{ amount: 5e121, type: "gold" },
|
||||
{ amount: 1.5e118, type: "essence" },
|
||||
{ amount: 7e113, type: "crystals" },
|
||||
{ targetId: "infinity_ranger_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "the_absolute",
|
||||
|
||||
@@ -23,7 +23,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
||||
zoneId: "verdant_vale",
|
||||
},
|
||||
{
|
||||
bonus: { type: "combat_power", value: 1.08 },
|
||||
bonus: { type: "combat_power", value: 1.12 },
|
||||
description:
|
||||
"A ward fashioned from bark older than the kingdom. Its presence on the battlefield is not merely physical — something ancient watches through it.",
|
||||
id: "elder_bark_shield",
|
||||
@@ -75,7 +75,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
||||
zoneId: "frozen_peaks",
|
||||
},
|
||||
{
|
||||
bonus: { type: "gold_income", value: 1.1 },
|
||||
bonus: { type: "gold_income", value: 1.15 },
|
||||
description:
|
||||
"The void shard set in frost crystal, creating something that should not be possible: a stable window into the spaces between spaces. Profitable beyond what physics suggests.",
|
||||
id: "void_fragment_amulet",
|
||||
@@ -231,7 +231,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
||||
zoneId: "infernal_court",
|
||||
},
|
||||
{
|
||||
bonus: { type: "essence_income", value: 1.15 },
|
||||
bonus: { type: "essence_income", value: 1.2 },
|
||||
description:
|
||||
"Soul residue and demon ichor worked into a catalyst that draws the essence from everything around it — gently, without drawing the court's attention. Usually.",
|
||||
id: "soul_bound_catalyst",
|
||||
@@ -492,6 +492,19 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
||||
],
|
||||
zoneId: "abyssal_trench",
|
||||
},
|
||||
{
|
||||
bonus: { type: "click_power", value: 1.38 },
|
||||
description:
|
||||
"A primeval relic submerged at the absolute boundary of existence alongside omega crystals and boundary shards — the first and last thing, unified. Every action your guild takes through it is simultaneously the most ancient and most final thing that has ever happened. It does not miss.",
|
||||
id: "primal_omega_lens",
|
||||
name: "Primal Omega Lens",
|
||||
requiredMaterials: [
|
||||
{ materialId: "primeval_relic", quantity: 2 },
|
||||
{ materialId: "boundary_shard", quantity: 4 },
|
||||
{ materialId: "omega_crystal", quantity: 2 },
|
||||
],
|
||||
zoneId: "the_absolute",
|
||||
},
|
||||
{
|
||||
bonus: { type: "combat_power", value: 1.4 },
|
||||
description:
|
||||
@@ -508,6 +521,18 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
||||
},
|
||||
|
||||
// Zone 18: the_absolute
|
||||
{
|
||||
bonus: { type: "click_power", value: 1.28 },
|
||||
description:
|
||||
"Absolute fragments ground and set in an omega crystal lattice — an instrument of pure finality. Every action your guild takes through it carries the weight of an ending. It does not miss.",
|
||||
id: "absolute_focus",
|
||||
name: "Absolute Focus",
|
||||
requiredMaterials: [
|
||||
{ materialId: "absolute_fragment", quantity: 8 },
|
||||
{ materialId: "omega_crystal", quantity: 3 },
|
||||
],
|
||||
zoneId: "the_absolute",
|
||||
},
|
||||
{
|
||||
bonus: { type: "gold_income", value: 1.3 },
|
||||
description:
|
||||
|
||||
@@ -11,7 +11,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
// ── Income multipliers ──────────────────────────────────────────────────────
|
||||
{
|
||||
category: "income",
|
||||
cost: 5,
|
||||
cost: 2,
|
||||
description:
|
||||
"The echoes of past runs linger, amplifying your guild's income by 25%.",
|
||||
id: "echo_income_1",
|
||||
@@ -20,7 +20,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "income",
|
||||
cost: 10,
|
||||
cost: 4,
|
||||
description:
|
||||
"Your transcendent experience resonates through your guild, boosting income by 50%.",
|
||||
id: "echo_income_2",
|
||||
@@ -29,7 +29,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "income",
|
||||
cost: 20,
|
||||
cost: 8,
|
||||
description:
|
||||
"The harmony of multiple timelines surges through your guild, doubling its income.",
|
||||
id: "echo_income_3",
|
||||
@@ -38,7 +38,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "income",
|
||||
cost: 40,
|
||||
cost: 16,
|
||||
description:
|
||||
"Ethereal energy overflows from your transcendence, tripling your guild's income.",
|
||||
id: "echo_income_4",
|
||||
@@ -47,7 +47,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "income",
|
||||
cost: 80,
|
||||
cost: 32,
|
||||
description:
|
||||
"The infinite chorus of every run you've ever played amplifies your guild fivefold.",
|
||||
id: "echo_income_5",
|
||||
@@ -58,7 +58,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
// ── Combat multipliers ──────────────────────────────────────────────────────
|
||||
{
|
||||
category: "combat",
|
||||
cost: 5,
|
||||
cost: 2,
|
||||
description:
|
||||
"Memories of countless battles harden your adventurers, increasing party DPS by 25%.",
|
||||
id: "echo_combat_1",
|
||||
@@ -67,7 +67,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "combat",
|
||||
cost: 15,
|
||||
cost: 6,
|
||||
description:
|
||||
"Veterans of transcendence know how to fight smarter, boosting party DPS by 50%.",
|
||||
id: "echo_combat_2",
|
||||
@@ -76,7 +76,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "combat",
|
||||
cost: 35,
|
||||
cost: 12,
|
||||
description:
|
||||
"Your warriors carry the strength of every fallen timeline, doubling party DPS.",
|
||||
id: "echo_combat_3",
|
||||
@@ -87,7 +87,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
// ── Prestige threshold reductions ──────────────────────────────────────────
|
||||
{
|
||||
category: "prestige_threshold",
|
||||
cost: 8,
|
||||
cost: 3,
|
||||
description:
|
||||
"Experience from past lives shortens the road to prestige — threshold reduced by 10%.",
|
||||
id: "echo_prestige_threshold_1",
|
||||
@@ -96,7 +96,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "prestige_threshold",
|
||||
cost: 20,
|
||||
cost: 6,
|
||||
description:
|
||||
"You've walked this path so many times you know every shortcut — threshold reduced by 20%.",
|
||||
id: "echo_prestige_threshold_2",
|
||||
@@ -107,7 +107,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
// ── Prestige runestone multipliers ─────────────────────────────────────────
|
||||
{
|
||||
category: "prestige_runestones",
|
||||
cost: 8,
|
||||
cost: 3,
|
||||
description:
|
||||
"Transcendent insight attunes you to the runestones, earning 50% more per prestige.",
|
||||
id: "echo_prestige_runestones_1",
|
||||
@@ -116,7 +116,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "prestige_runestones",
|
||||
cost: 20,
|
||||
cost: 6,
|
||||
description:
|
||||
"You have mastered the art of runestone crafting, doubling your prestige runestone yield.",
|
||||
id: "echo_prestige_runestones_2",
|
||||
@@ -127,7 +127,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
// ── Echo meta multipliers ───────────────────────────────────────────────────
|
||||
{
|
||||
category: "echo_meta",
|
||||
cost: 50,
|
||||
cost: 25,
|
||||
description:
|
||||
"Your transcendence resonates deeper, amplifying future echo yields by 25%.",
|
||||
id: "echo_meta_1",
|
||||
@@ -136,7 +136,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "echo_meta",
|
||||
cost: 150,
|
||||
cost: 75,
|
||||
description:
|
||||
"Each loop of existence makes the next more powerful — future echo yields +50%.",
|
||||
id: "echo_meta_2",
|
||||
@@ -145,7 +145,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
||||
},
|
||||
{
|
||||
category: "echo_meta",
|
||||
cost: 400,
|
||||
cost: 200,
|
||||
description:
|
||||
"You have mastered the infinite spiral of transcendence, doubling all future echo yields.",
|
||||
id: "echo_meta_3",
|
||||
|
||||
@@ -102,12 +102,23 @@ prestigeRouter.post("/", async(context) => {
|
||||
}).length;
|
||||
|
||||
const now = Date.now();
|
||||
await prisma.gameState.update({
|
||||
const { updatedAt } = record;
|
||||
|
||||
/*
|
||||
* Use the record's current updatedAt as an optimistic lock — if another
|
||||
* concurrent prestige request already committed, this update will match
|
||||
* 0 rows and we can safely reject the duplicate without a double webhook.
|
||||
*/
|
||||
const updateResult = await prisma.gameState.updateMany({
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */
|
||||
data: { state: finalState as object, updatedAt: now },
|
||||
where: { discordId },
|
||||
where: { discordId, updatedAt },
|
||||
});
|
||||
|
||||
if (updateResult.count === 0) {
|
||||
return context.json({ error: "Prestige already in progress" }, 409);
|
||||
}
|
||||
|
||||
await prisma.player.update({
|
||||
data: {
|
||||
characterName: state.player.characterName,
|
||||
@@ -136,6 +147,18 @@ prestigeRouter.post("/", async(context) => {
|
||||
|
||||
const prestigeCount = prestigeData.count;
|
||||
void logger.metric("prestige", 1, { discordId, prestigeCount });
|
||||
|
||||
const playerRecord = await prisma.player.findUnique({
|
||||
select: { profileSettings: true },
|
||||
where: { discordId },
|
||||
});
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Runtime shape check for JSON field */
|
||||
const playerSettings = playerRecord?.profileSettings as
|
||||
Record<string, unknown> | null | undefined;
|
||||
const announcementsEnabled
|
||||
= playerSettings?.enablePrestigeAnnouncements !== false;
|
||||
|
||||
if (announcementsEnabled) {
|
||||
void postMilestoneWebhook(discordId, "prestige", {
|
||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||
/* v8 ignore next -- @preserve */
|
||||
@@ -147,6 +170,7 @@ prestigeRouter.post("/", async(context) => {
|
||||
/* v8 ignore next 2 -- @preserve */
|
||||
transcendence: prestigeState.transcendence?.count ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json({
|
||||
milestoneRunestones: milestoneRunestones,
|
||||
|
||||
@@ -47,6 +47,7 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
||||
: "suffix";
|
||||
return {
|
||||
enableNotifications: rawObject.enableNotifications === true,
|
||||
enablePrestigeAnnouncements: rawObject.enablePrestigeAnnouncements !== false,
|
||||
enableSounds: rawObject.enableSounds === true,
|
||||
numberFormat: numberFormat,
|
||||
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
||||
@@ -222,6 +223,7 @@ profileRouter.put("/", authMiddleware, async(context) => {
|
||||
: "suffix";
|
||||
const profileSettings: ProfileSettings = {
|
||||
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
||||
enablePrestigeAnnouncements: body.profileSettings.enablePrestigeAnnouncements ?? true,
|
||||
enableSounds: body.profileSettings.enableSounds ?? false,
|
||||
numberFormat: numberFormat,
|
||||
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
||||
|
||||
@@ -71,8 +71,7 @@ const shuffleWithSeed = <T>(array: Array<T>, seed: number): Array<T> => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const challengeTypes: Array<DailyChallengeType> = [
|
||||
"clicks",
|
||||
const progressionChallengeTypes: Array<DailyChallengeType> = [
|
||||
"bossesDefeated",
|
||||
"questsCompleted",
|
||||
"prestige",
|
||||
@@ -80,7 +79,8 @@ const challengeTypes: Array<DailyChallengeType> = [
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns An array of 3 DailyChallenge objects.
|
||||
*/
|
||||
@@ -88,8 +88,10 @@ const generateDailyChallenges = (
|
||||
dateString: string,
|
||||
): Array<DailyChallenge> => {
|
||||
const seed = dateSeed(dateString);
|
||||
const selectedTypes = shuffleWithSeed([ ...challengeTypes ], seed).
|
||||
slice(0, 3);
|
||||
const selectedTypes: Array<DailyChallengeType> = [
|
||||
"clicks",
|
||||
...shuffleWithSeed([ ...progressionChallengeTypes ], seed).slice(0, 2),
|
||||
];
|
||||
|
||||
return selectedTypes.map((type, index) => {
|
||||
const templates = dailyChallengeTemplates.filter((template) => {
|
||||
|
||||
@@ -15,14 +15,21 @@ import type {
|
||||
} from "@elysium/types";
|
||||
|
||||
const basePrestigeGoldThreshold = 1_000_000;
|
||||
const thresholdScaleFactor = 5;
|
||||
const runestonesPerPrestigeLevel = 10;
|
||||
const runestonesPerPrestigeLevel = 15;
|
||||
const milestoneInterval = 5;
|
||||
const milestoneRunestonesPerInterval = 25;
|
||||
|
||||
/*
|
||||
* Hard cap on the base runestone yield (before multipliers) to prevent
|
||||
* extreme AFK accumulation from producing game-breaking runestone counts.
|
||||
* With all upgrades (5.625× max) this caps out at ~1,125 per prestige.
|
||||
*/
|
||||
const maxBaseRunestones = 200;
|
||||
|
||||
/**
|
||||
* Calculates the gold threshold required for the next prestige.
|
||||
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
|
||||
* Formula: BASE * (count + 1)^2 — polynomial growth that peaks around prestige 8–10
|
||||
* then gets easier as the production multiplier overtakes it.
|
||||
* @param prestigeCount - The current number of prestiges completed.
|
||||
* @param thresholdMultiplier - An optional echo-upgrade multiplier applied to the threshold.
|
||||
* @returns The gold amount required to prestige.
|
||||
@@ -33,7 +40,7 @@ const calculatePrestigeThreshold = (
|
||||
): number => {
|
||||
return (
|
||||
basePrestigeGoldThreshold
|
||||
* Math.pow(thresholdScaleFactor, prestigeCount)
|
||||
* Math.pow(prestigeCount + 1, 2)
|
||||
* thresholdMultiplier
|
||||
);
|
||||
};
|
||||
@@ -107,7 +114,9 @@ interface RunestoneParameters {
|
||||
|
||||
/**
|
||||
* Calculates how many runestones the player earns from a prestige.
|
||||
* Formula: floor(sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL * runestoneMultiplier.
|
||||
* Formula: min(floor(cbrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL, MAX_BASE) * multipliers.
|
||||
* Uses cube root for stronger diminishing returns than sqrt, and caps the base before multipliers
|
||||
* to prevent extended AFK sessions from producing runestone windfalls.
|
||||
* @param parameters - The parameters for the runestone calculation.
|
||||
* @param parameters.totalGoldEarned - The total gold earned in the current run.
|
||||
* @param parameters.prestigeCount - The current prestige count.
|
||||
@@ -123,9 +132,11 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
|
||||
echoRunestoneMultiplier = 1,
|
||||
} = parameters;
|
||||
const threshold = calculatePrestigeThreshold(prestigeCount);
|
||||
const base
|
||||
= Math.floor(Math.sqrt(totalGoldEarned / threshold))
|
||||
* runestonesPerPrestigeLevel;
|
||||
const base = Math.min(
|
||||
Math.floor(Math.cbrt(totalGoldEarned / threshold))
|
||||
* runestonesPerPrestigeLevel,
|
||||
maxBaseRunestones,
|
||||
);
|
||||
const runestoneMult = getCategoryMultiplier(
|
||||
purchasedUpgradeIds,
|
||||
"runestones",
|
||||
@@ -135,14 +146,15 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
|
||||
|
||||
/**
|
||||
* Calculates the new prestige production multiplier.
|
||||
* Formula: 1.15^prestigeCount — exponential scaling per prestige.
|
||||
* Formula: 1.25^prestigeCount — exponential scaling per prestige that eventually
|
||||
* overtakes the polynomial threshold growth, making late prestiges progressively easier.
|
||||
* @param prestigeCount - The new prestige count.
|
||||
* @returns The production multiplier for the new prestige level.
|
||||
*/
|
||||
const calculateProductionMultiplier = (
|
||||
prestigeCount: number,
|
||||
): number => {
|
||||
return Math.pow(1.15, prestigeCount);
|
||||
return Math.pow(1.25, prestigeCount);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -251,6 +263,7 @@ const buildPostPrestigeState = (
|
||||
* Preserve automation preferences across prestige — the player explicitly
|
||||
* opted into these settings and would not expect them to silently reset.
|
||||
*/
|
||||
autoAdventurer: currentState.autoAdventurer ?? false,
|
||||
autoBoss: currentState.autoBoss ?? false,
|
||||
|
||||
autoQuest: currentState.autoQuest ?? false,
|
||||
|
||||
@@ -20,7 +20,7 @@ const finalBossId = "the_absolute_one";
|
||||
/**
|
||||
* Base constant used in the echo yield formula.
|
||||
*/
|
||||
const echoFormulaConstant = 853;
|
||||
const echoFormulaConstant = 224;
|
||||
|
||||
const getCategoryMultiplier = (
|
||||
purchasedIds: Array<string>,
|
||||
|
||||
@@ -595,6 +595,18 @@ describe("debug route", () => {
|
||||
expect(adventurer?.unlocked).toBe(true);
|
||||
});
|
||||
|
||||
it("patches adventurer stats when only name has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
adventurers: [{ id: "militia", count: 5, unlocked: true, baseCost: 100, goldPerSecond: 0.7, essencePerSecond: 0, combatPower: 3, level: 2, name: "Old Name", class: "warrior" }] as GameState["adventurers"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { adventurerStatsPatched: number };
|
||||
expect(body.adventurerStatsPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips adventurer stat patching for adventurers not in defaults", async () => {
|
||||
const state = makeState({
|
||||
adventurers: [{ id: "nonexistent_adventurer", count: 0, unlocked: false, baseCost: 1, goldPerSecond: 1, essencePerSecond: 1, combatPower: 1, level: 1, name: "Ghost", class: "warrior" }] as GameState["adventurers"],
|
||||
@@ -816,6 +828,18 @@ describe("debug route", () => {
|
||||
expect(quest?.status).toBe("available");
|
||||
});
|
||||
|
||||
it("patches quest stats when only combatPowerRequired has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
quests: [{ id: "haunted_mine", status: "available", rewards: [], durationSeconds: 900, name: "The Haunted Mine", description: "An abandoned mine is rich with crystal deposits — if you dare brave its ghosts.", prerequisiteIds: ["goblin_camp"], zoneId: "verdant_vale", combatPowerRequired: 0 }] as GameState["quests"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { questsPatched: number };
|
||||
expect(body.questsPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips quest stat patching for quests not in defaults", async () => {
|
||||
const state = makeState({
|
||||
quests: [{ id: "nonexistent_quest_xyz", status: "available", rewards: [], durationSeconds: 1, name: "Ghost", description: "Old", prerequisiteIds: [], zoneId: "old_zone", combatPowerRequired: 0 }] as GameState["quests"],
|
||||
@@ -845,6 +869,18 @@ describe("debug route", () => {
|
||||
expect(boss?.currentHp).toBe(100);
|
||||
});
|
||||
|
||||
it("patches boss stats when only bountyRunestones has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
bosses: [{ id: "troll_king", status: "available", currentHp: 1000, maxHp: 1000, upgradeRewards: [], bountyRunestonesClaimed: false, damagePerSecond: 5, goldReward: 10_000, essenceReward: 25, crystalReward: 0, equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, zoneId: "verdant_vale", bountyRunestones: 99, name: "The Troll King", description: "Gruk the Immovable has terrorised the trade roads for decades. Merchants will pay handsomely for his head." }] as GameState["bosses"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { bossesPatched: number };
|
||||
expect(body.bossesPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips boss stat patching for bosses not in defaults", async () => {
|
||||
const state = makeState({
|
||||
bosses: [{ id: "nonexistent_boss_xyz", status: "available", currentHp: 100, maxHp: 1, upgradeRewards: [], bountyRunestonesClaimed: false, damagePerSecond: 1, goldReward: 1, essenceReward: 1, crystalReward: 1, equipmentRewards: [], prestigeRequirement: 0, zoneId: "old_zone", bountyRunestones: 0, name: "Ghost", description: "Old" }] as GameState["bosses"],
|
||||
@@ -872,6 +908,18 @@ describe("debug route", () => {
|
||||
expect(zone?.status).toBe("unlocked");
|
||||
});
|
||||
|
||||
it("patches zone stats when only unlockQuestId has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
zones: [{ id: "verdant_vale", status: "unlocked", name: "The Verdant Vale", description: "Rolling green hills and ancient forests stretch to the horizon. This is where your guild takes its first steps — trade roads in need of clearing, goblin camps to rout, and an undead queen stirring in the north.", emoji: "🌿", unlockBossId: null, unlockQuestId: "wrong_quest" }] as GameState["zones"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { zonesPatched: number };
|
||||
expect(body.zonesPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips zone stat patching for zones not in defaults", async () => {
|
||||
const state = makeState({
|
||||
zones: [{ id: "nonexistent_zone_xyz", status: "unlocked", name: "Ghost", description: "Old", emoji: "❓", unlockBossId: null, unlockQuestId: null }] as GameState["zones"],
|
||||
@@ -901,6 +949,18 @@ describe("debug route", () => {
|
||||
expect(upgrade?.unlocked).toBe(true);
|
||||
});
|
||||
|
||||
it("patches upgrade stats when only costCrystals has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
upgrades: [{ id: "click_2", purchased: false, unlocked: false, multiplier: 2, name: "Battle Hardened", description: "Years of combat sharpen your instincts. Doubles click power again.", target: "click", adventurerId: undefined, costGold: 1000, costEssence: 0, costCrystals: 99 }] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { upgradesPatched: number };
|
||||
expect(body.upgradesPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips upgrade stat patching for upgrades not in defaults", async () => {
|
||||
const state = makeState({
|
||||
upgrades: [{ id: "nonexistent_upgrade_xyz", purchased: false, unlocked: false, multiplier: 0.1, name: "Ghost", description: "Old", target: "click", adventurerId: undefined, costGold: 1, costEssence: 0, costCrystals: 0 }] as GameState["upgrades"],
|
||||
@@ -929,6 +989,30 @@ describe("debug route", () => {
|
||||
expect(item?.equipped).toBe(false);
|
||||
});
|
||||
|
||||
it("patches equipment stats when only cost has changed (exercises name/desc/type/rarity/bonus OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
equipment: [{ id: "shadow_dagger", owned: true, equipped: false, name: "Shadow Dagger", description: "Forged in the Shadow Marshes from condensed darkness. It strikes before it is seen.", type: "weapon", rarity: "epic", bonus: { combatMultiplier: 1.65 }, cost: { crystals: 99, essence: 500, gold: 0 }, setId: "shadow_infiltrator" }] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { equipmentPatched: number };
|
||||
expect(body.equipmentPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("patches equipment stats when only setId has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
equipment: [{ id: "iron_sword", owned: true, equipped: false, name: "Iron Sword", description: "A sturdy weapon issued to veterans of the guild.", type: "weapon", rarity: "rare", bonus: { combatMultiplier: 1.25 }, cost: undefined, setId: "old_set" }] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { equipmentPatched: number };
|
||||
expect(body.equipmentPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips equipment stat patching for items not in defaults", async () => {
|
||||
const state = makeState({
|
||||
equipment: [{ id: "nonexistent_item_xyz", owned: false, equipped: false, name: "Ghost Sword", description: "Old", type: "weapon", rarity: "common", bonus: { type: "click_power", value: 0 }, cost: undefined, setId: undefined }] as GameState["equipment"],
|
||||
@@ -957,6 +1041,18 @@ describe("debug route", () => {
|
||||
expect(achievement?.unlockedAt).toBeNull();
|
||||
});
|
||||
|
||||
it("patches achievement stats when only reward has changed (exercises all earlier OR conditions)", async () => {
|
||||
const state = makeState({
|
||||
achievements: [{ id: "first_click", unlockedAt: null, name: "First Strike", description: "Click the Guild Hall for the first time.", icon: "👆", condition: { amount: 1, type: "totalClicks" }, reward: { crystals: 999 } }] as GameState["achievements"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await syncNewContent();
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { achievementsPatched: number };
|
||||
expect(body.achievementsPatched).toBe(1);
|
||||
});
|
||||
|
||||
it("skips achievement stat patching for achievements not in defaults", async () => {
|
||||
const state = makeState({
|
||||
achievements: [{ id: "nonexistent_achievement_xyz", unlockedAt: null, name: "Ghost", description: "Old", icon: "❓", condition: { type: "totalClicks", amount: 1 }, reward: undefined }] as GameState["achievements"],
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { GameState } from "@elysium/types";
|
||||
|
||||
vi.mock("../../src/db/client.js", () => ({
|
||||
prisma: {
|
||||
player: { update: vi.fn() },
|
||||
gameState: { findUnique: vi.fn(), update: vi.fn() },
|
||||
player: { findUnique: vi.fn(), update: vi.fn() },
|
||||
gameState: { findUnique: vi.fn(), update: vi.fn(), updateMany: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -47,8 +47,8 @@ const makeState = (overrides: Partial<GameState> = {}): GameState => ({
|
||||
describe("prestige route", () => {
|
||||
let app: Hono;
|
||||
let prisma: {
|
||||
player: { update: ReturnType<typeof vi.fn> };
|
||||
gameState: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
||||
player: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
||||
gameState: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn>; updateMany: ReturnType<typeof vi.fn> };
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -83,8 +83,8 @@ describe("prestige route", () => {
|
||||
|
||||
it("returns runestones on successful prestige", async () => {
|
||||
const state = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
const res = await post("");
|
||||
expect(res.status).toBe(200);
|
||||
@@ -93,6 +93,14 @@ describe("prestige route", () => {
|
||||
expect(body.runestones).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("returns 409 when a concurrent prestige already committed", async () => {
|
||||
const state = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 0 } as never);
|
||||
const res = await post("");
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
|
||||
it("returns 500 when the database throws during prestige", async () => {
|
||||
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
||||
const res = await post("");
|
||||
@@ -112,14 +120,26 @@ describe("prestige route", () => {
|
||||
challenges: [{ id: "prestige_challenge", type: "prestige", target: 2, progress: 0, completed: false, crystalReward: 5 }],
|
||||
} as GameState["dailyChallenges"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
const res = await post("");
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { runestones: number; newPrestigeCount: number };
|
||||
expect(body.newPrestigeCount).toBe(1);
|
||||
});
|
||||
|
||||
it("skips webhook when enablePrestigeAnnouncements is false", async () => {
|
||||
const { postMilestoneWebhook } = await import("../../src/services/webhook.js");
|
||||
const state = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce({ profileSettings: { enablePrestigeAnnouncements: false } } as never);
|
||||
const res = await post("");
|
||||
expect(res.status).toBe(200);
|
||||
expect(postMilestoneWebhook).not.toHaveBeenCalledWith(expect.anything(), "prestige", expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /buy-upgrade", () => {
|
||||
|
||||
@@ -158,7 +158,7 @@ describe("transcendence route", () => {
|
||||
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { echoesRemaining: number; purchasedUpgradeIds: string[] };
|
||||
expect(body.echoesRemaining).toBe(95); // 100 - 5
|
||||
expect(body.echoesRemaining).toBe(98); // 100 - 2
|
||||
expect(body.purchasedUpgradeIds).toContain("echo_income_1");
|
||||
});
|
||||
|
||||
|
||||
@@ -46,13 +46,24 @@ describe("generateDailyChallenges", () => {
|
||||
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 () => {
|
||||
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");
|
||||
// They should differ in at least one challenge ID (types vary by seed)
|
||||
expect(day1.map((c) => c.type)).not.toEqual(day2.map((c) => c.type));
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,15 +55,18 @@ const makeMinimalState = (overrides: Partial<GameState> = {}): GameState =>
|
||||
|
||||
describe("calculatePrestigeThreshold", () => {
|
||||
it("returns base threshold at count 0", () => {
|
||||
// base × (0+1)^2 = 1_000_000 × 1 = 1_000_000
|
||||
expect(calculatePrestigeThreshold(0)).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("returns 5× at count 1", () => {
|
||||
expect(calculatePrestigeThreshold(1)).toBe(5_000_000);
|
||||
it("returns 4× base at count 1", () => {
|
||||
// base × (1+1)^2 = 1_000_000 × 4 = 4_000_000
|
||||
expect(calculatePrestigeThreshold(1)).toBe(4_000_000);
|
||||
});
|
||||
|
||||
it("returns 25× at count 2", () => {
|
||||
expect(calculatePrestigeThreshold(2)).toBe(25_000_000);
|
||||
it("returns 9× base at count 2", () => {
|
||||
// base × (2+1)^2 = 1_000_000 × 9 = 9_000_000
|
||||
expect(calculatePrestigeThreshold(2)).toBe(9_000_000);
|
||||
});
|
||||
|
||||
it("applies threshold multiplier correctly", () => {
|
||||
@@ -99,21 +102,27 @@ describe("isEligibleForPrestige", () => {
|
||||
|
||||
describe("calculateRunestones", () => {
|
||||
it("calculates basic runestones formula", () => {
|
||||
// floor(sqrt(4_000_000 / 1_000_000)) × 10 = floor(2) × 10 = 20
|
||||
// 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: [] });
|
||||
expect(result).toBe(20);
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
|
||||
it("applies echo runestone multiplier", () => {
|
||||
// floor(sqrt(4) × 10) = 20; × 2 = 40
|
||||
// floor(cbrt(4)) × 15 = 15; × 2 = 30
|
||||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
|
||||
expect(result).toBe(40);
|
||||
expect(result).toBe(30);
|
||||
});
|
||||
|
||||
it("applies purchased runestone upgrade multiplier", () => {
|
||||
// With "runestones_1" purchased (multiplier 1.25): floor(20 × 1.25) = 25
|
||||
// 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"] });
|
||||
expect(result).toBeGreaterThan(20);
|
||||
expect(result).toBe(18);
|
||||
});
|
||||
|
||||
it("caps base runestones before multipliers", () => {
|
||||
// cbrt(9_261_000_000 / 1_000_000) = cbrt(9261) = 21 → 21 × 10 = 210, capped at 200
|
||||
const result = calculateRunestones({ totalGoldEarned: 9_261_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
|
||||
expect(result).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,12 +131,12 @@ describe("calculateProductionMultiplier", () => {
|
||||
expect(calculateProductionMultiplier(0)).toBe(1);
|
||||
});
|
||||
|
||||
it("returns 1.15 at count 1", () => {
|
||||
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.15);
|
||||
it("returns 1.25 at count 1", () => {
|
||||
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.25);
|
||||
});
|
||||
|
||||
it("scales exponentially", () => {
|
||||
expect(calculateProductionMultiplier(10)).toBeCloseTo(Math.pow(1.15, 10));
|
||||
expect(calculateProductionMultiplier(10)).toBeCloseTo(Math.pow(1.25, 10));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -97,20 +97,21 @@ describe("isEligibleForTranscendence", () => {
|
||||
|
||||
describe("calculateEchoes", () => {
|
||||
it("handles prestige count of 0 by treating it as 1", () => {
|
||||
// safeCount = max(0, 1) = 1; floor(853 / sqrt(1)) = 853
|
||||
expect(calculateEchoes(0, 1)).toBe(853);
|
||||
// safeCount = max(0, 1) = 1; floor(224 / sqrt(1)) = 224
|
||||
expect(calculateEchoes(0, 1)).toBe(224);
|
||||
});
|
||||
|
||||
it("calculates echoes at count 1", () => {
|
||||
expect(calculateEchoes(1, 1)).toBe(853);
|
||||
// floor(224 / sqrt(1)) = 224
|
||||
expect(calculateEchoes(1, 1)).toBe(224);
|
||||
});
|
||||
|
||||
it("decreases echoes with higher prestige count", () => {
|
||||
const echoesAt1 = calculateEchoes(1, 1);
|
||||
const echoesAt4 = calculateEchoes(4, 1);
|
||||
expect(echoesAt4).toBeLessThan(echoesAt1);
|
||||
// floor(853 / sqrt(4)) = floor(853 / 2) = 426
|
||||
expect(echoesAt4).toBe(426);
|
||||
// floor(224 / sqrt(4)) = floor(224 / 2) = 112
|
||||
expect(echoesAt4).toBe(112);
|
||||
});
|
||||
|
||||
it("applies echoMetaMultiplier", () => {
|
||||
@@ -118,6 +119,11 @@ describe("calculateEchoes", () => {
|
||||
const withMult = calculateEchoes(1, 2);
|
||||
expect(withMult).toBe(base * 2);
|
||||
});
|
||||
|
||||
it("returns 50 echoes at the target prestige 20", () => {
|
||||
// floor(224 / sqrt(20)) = floor(224 / 4.472) = floor(50.09) = 50
|
||||
expect(calculateEchoes(20, 1)).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPostTranscendenceState", () => {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
/* eslint-disable complexity -- Complex component with many render paths */
|
||||
import { type JSX, useState } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { computeEffectiveAdventurerStats } from "../../engine/tick.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
import { LockToggle } from "../ui/lockToggle.js";
|
||||
import type { Adventurer } from "@elysium/types";
|
||||
@@ -76,12 +77,19 @@ const computeMaxAffordable = (adventurer: Adventurer, gold: number): number => {
|
||||
return quantity;
|
||||
};
|
||||
|
||||
interface EffectiveAdventurerStats {
|
||||
readonly combatPower: number;
|
||||
readonly essencePerSecond: number;
|
||||
readonly goldPerSecond: number;
|
||||
}
|
||||
|
||||
interface AdventurerCardProperties {
|
||||
readonly adventurer: Adventurer;
|
||||
readonly currentGold: number;
|
||||
readonly batchSize: BatchSize;
|
||||
readonly unlockHint: string | undefined;
|
||||
readonly formatNumber: (n: number)=> string;
|
||||
readonly effectiveStats: EffectiveAdventurerStats;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +100,7 @@ interface AdventurerCardProperties {
|
||||
* @param props.batchSize - The selected batch size.
|
||||
* @param props.unlockHint - Optional quest name that unlocks this adventurer.
|
||||
* @param props.formatNumber - The number formatting utility function.
|
||||
* @param props.effectiveStats - The post-multiplier per-unit stats.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const AdventurerCard = ({
|
||||
@@ -100,6 +109,7 @@ const AdventurerCard = ({
|
||||
batchSize,
|
||||
unlockHint,
|
||||
formatNumber,
|
||||
effectiveStats,
|
||||
}: AdventurerCardProperties): JSX.Element => {
|
||||
const { buyAdventurer } = useGame();
|
||||
|
||||
@@ -134,17 +144,17 @@ const AdventurerCard = ({
|
||||
<div className="adventurer-info">
|
||||
<h3>{adventurer.name}</h3>
|
||||
<p>
|
||||
{formatNumber(adventurer.goldPerSecond)}
|
||||
{formatNumber(effectiveStats.goldPerSecond)}
|
||||
{" gold/s each"}
|
||||
</p>
|
||||
{adventurer.essencePerSecond > 0
|
||||
&& <p>
|
||||
{formatNumber(adventurer.essencePerSecond)}
|
||||
{formatNumber(effectiveStats.essencePerSecond)}
|
||||
{" essence/s each"}
|
||||
</p>
|
||||
}
|
||||
<p>
|
||||
{formatNumber(adventurer.combatPower)}
|
||||
{formatNumber(effectiveStats.combatPower)}
|
||||
{" combat power each"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -280,6 +290,10 @@ const AdventurerPanel = (): JSX.Element => {
|
||||
adventurer={adventurer}
|
||||
batchSize={batchSize}
|
||||
currentGold={state.resources.gold}
|
||||
effectiveStats={computeEffectiveAdventurerStats(
|
||||
state,
|
||||
adventurer.id,
|
||||
)}
|
||||
formatNumber={formatNumber}
|
||||
key={adventurer.id}
|
||||
unlockHint={adventurerUnlockHints.get(adventurer.id)}
|
||||
|
||||
@@ -225,6 +225,10 @@ const EditProfileModal = ({
|
||||
void handleNotificationsEnable();
|
||||
}
|
||||
|
||||
function handlePrestigeAnnouncementsToggle(): void {
|
||||
toggleSetting("enablePrestigeAnnouncements");
|
||||
}
|
||||
|
||||
const isSaveDisabled = saving || characterName.trim() === "";
|
||||
|
||||
let saveLabel = "Save Profile";
|
||||
@@ -417,6 +421,23 @@ const EditProfileModal = ({
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`stat-toggle-btn ${
|
||||
profileSettings.enablePrestigeAnnouncements
|
||||
? "stat-toggle-on"
|
||||
: "stat-toggle-off"
|
||||
}`}
|
||||
onClick={handlePrestigeAnnouncementsToggle}
|
||||
type="button"
|
||||
>
|
||||
<span>{"⭐ Prestige Bot Announcements"}</span>
|
||||
<span className="stat-toggle-indicator">
|
||||
{profileSettings.enablePrestigeAnnouncements
|
||||
? "✓ On"
|
||||
: "Off"
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="edit-profile-section">
|
||||
|
||||
@@ -12,25 +12,27 @@ import { useState, type JSX } from "react";
|
||||
import { prestige } from "../../api/client.js";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import {
|
||||
PRESTIGE_UPGRADES,
|
||||
PRESTIGE_UPGRADE_CATEGORY_LABELS,
|
||||
PRESTIGE_UPGRADES,
|
||||
} from "../../data/prestigeUpgrades.js";
|
||||
import {
|
||||
computeProjectedRunestones,
|
||||
} from "../../engine/tick.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
import { sendNotification } from "../../utils/notification.js";
|
||||
import { playSound } from "../../utils/sound.js";
|
||||
import type { PrestigeUpgradeCategory } from "@elysium/types";
|
||||
|
||||
const baseThreshold = 1_000_000;
|
||||
const thresholdScale = 5;
|
||||
const runestonesPerLevel = 10;
|
||||
|
||||
/**
|
||||
* Calculates the prestige threshold for a given prestige count.
|
||||
* Mirrors the server formula: BASE * (count + 1)^2.
|
||||
* @param prestigeCount - The current prestige count.
|
||||
* @returns The required gold to prestige.
|
||||
*/
|
||||
const calculateThreshold = (prestigeCount: number): number => {
|
||||
return baseThreshold * Math.pow(thresholdScale, prestigeCount);
|
||||
return baseThreshold * Math.pow(prestigeCount + 1, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -42,32 +44,6 @@ const calculateProductionMultiplier = (prestigeCount: number): number => {
|
||||
return Math.pow(1.15, prestigeCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the runestone preview for a prestige.
|
||||
* @param totalGoldEarned - Total gold earned this run.
|
||||
* @param prestigeCount - The current prestige count.
|
||||
* @param purchasedUpgradeIds - IDs of purchased prestige upgrades.
|
||||
* @returns The predicted runestone reward.
|
||||
*/
|
||||
const calculateRunestonePreview = (
|
||||
totalGoldEarned: number,
|
||||
prestigeCount: number,
|
||||
purchasedUpgradeIds: Array<string>,
|
||||
): number => {
|
||||
const threshold = calculateThreshold(prestigeCount);
|
||||
const base
|
||||
= Math.floor(Math.sqrt(totalGoldEarned / threshold)) * runestonesPerLevel;
|
||||
const runestoneMult = PRESTIGE_UPGRADES.filter((upgrade) => {
|
||||
return (
|
||||
upgrade.category === "runestones"
|
||||
&& purchasedUpgradeIds.includes(upgrade.id)
|
||||
);
|
||||
}).reduce((mult, upgrade) => {
|
||||
return mult * upgrade.multiplier;
|
||||
}, 1);
|
||||
return Math.floor(base * runestoneMult);
|
||||
};
|
||||
|
||||
const categoryOrder: Array<PrestigeUpgradeCategory> = [
|
||||
"income",
|
||||
"click",
|
||||
@@ -84,7 +60,7 @@ const categoryOrder: Array<PrestigeUpgradeCategory> = [
|
||||
const PrestigePanel = (): JSX.Element => {
|
||||
const {
|
||||
state,
|
||||
reload,
|
||||
reloadSilent,
|
||||
formatNumber,
|
||||
buyPrestigeUpgrade,
|
||||
enableNotifications,
|
||||
@@ -114,11 +90,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
const { autoAdventurer, prestige: prestigeData, player } = state;
|
||||
const threshold = calculateThreshold(prestigeData.count);
|
||||
const isEligible = player.totalGoldEarned >= threshold;
|
||||
const runestonePreview = calculateRunestonePreview(
|
||||
player.totalGoldEarned,
|
||||
prestigeData.count,
|
||||
prestigeData.purchasedUpgradeIds,
|
||||
);
|
||||
const runestonePreview = computeProjectedRunestones(state);
|
||||
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
|
||||
|
||||
async function handlePrestige(): Promise<void> {
|
||||
@@ -141,7 +113,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
`You've reached prestige level ${data.newPrestigeCount.toString()}!`,
|
||||
);
|
||||
}
|
||||
await reload();
|
||||
await reloadSilent();
|
||||
} catch (error_: unknown) {
|
||||
setPrestigeError(
|
||||
error_ instanceof Error
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
/* eslint-disable max-statements -- Many local variables needed for quest state */
|
||||
import { useState, type JSX } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { zoneFailureChance } from "../../engine/tick.js";
|
||||
import {
|
||||
computePartyCombatPower,
|
||||
zoneFailureChance,
|
||||
} from "../../engine/tick.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
import { LockToggle } from "../ui/lockToggle.js";
|
||||
import { ZoneSelector } from "./zoneSelector.js";
|
||||
@@ -208,7 +211,7 @@ const QuestPanel = (): JSX.Element => {
|
||||
);
|
||||
}
|
||||
|
||||
const { adventurers, autoQuest, bosses, quests, zones } = state;
|
||||
const { autoQuest, bosses, quests, zones } = state;
|
||||
|
||||
const activeZone = zones.find((zone) => {
|
||||
return zone.id === activeZoneId;
|
||||
@@ -226,11 +229,7 @@ const QuestPanel = (): JSX.Element => {
|
||||
: quests.find((quest) => {
|
||||
return quest.id === activeZone.unlockQuestId;
|
||||
});
|
||||
let partyCombatPower = 0;
|
||||
for (const adventurer of adventurers) {
|
||||
const contribution = adventurer.combatPower * adventurer.count;
|
||||
partyCombatPower = partyCombatPower + contribution;
|
||||
}
|
||||
const partyCombatPower = computePartyCombatPower(state);
|
||||
const zoneQuests = quests.filter(({ zoneId }) => {
|
||||
return zoneId === activeZoneId;
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
computeEssencePerSecond,
|
||||
computeGoldPerSecond,
|
||||
computePartyCombatPower,
|
||||
computeProjectedRunestones,
|
||||
} from "../../engine/tick.js";
|
||||
import type { Resource } from "@elysium/types";
|
||||
|
||||
@@ -89,10 +90,12 @@ const ResourceBar = ({
|
||||
let partyCombatPower = 0;
|
||||
let goldPerSecond = 0;
|
||||
let essencePerSecond = 0;
|
||||
let projectedRunestones = 0;
|
||||
if (state !== null) {
|
||||
partyCombatPower = computePartyCombatPower(state);
|
||||
goldPerSecond = computeGoldPerSecond(state);
|
||||
essencePerSecond = computeEssencePerSecond(state);
|
||||
projectedRunestones = computeProjectedRunestones(state);
|
||||
}
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
@@ -234,6 +237,13 @@ const ResourceBar = ({
|
||||
</span>
|
||||
<span className="resource-label">{"Runestones"}</span>
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"⭐"}</span>
|
||||
<span className="resource-value">
|
||||
{`+${formatNumber(projectedRunestones)}`}
|
||||
</span>
|
||||
<span className="resource-label">{"On Prestige"}</span>
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"⚔️"}</span>
|
||||
<span className="resource-value">
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
transcend as transcendApi,
|
||||
} from "../api/client.js";
|
||||
import { CODEX_ENTRIES } from "../data/codex.js";
|
||||
import { EXPLORATION_AREAS } from "../data/explorations.js";
|
||||
import { RECIPES } from "../data/recipes.js";
|
||||
import {
|
||||
RESOURCE_CAP,
|
||||
@@ -116,6 +117,9 @@ const applyBossResult = (
|
||||
}).
|
||||
filter(Boolean),
|
||||
);
|
||||
const newlyUnlockedZoneIds = new Set(unlockedZones.map((z) => {
|
||||
return z.id;
|
||||
}));
|
||||
|
||||
const challengeUpdate
|
||||
= previous.dailyChallenges === undefined
|
||||
@@ -216,6 +220,23 @@ const applyBossResult = (
|
||||
? { ...u, unlocked: true }
|
||||
: u;
|
||||
}),
|
||||
...newlyUnlockedZoneIds.size === 0 || previous.exploration === undefined
|
||||
? {}
|
||||
: {
|
||||
exploration: {
|
||||
...previous.exploration,
|
||||
areas: previous.exploration.areas.map((area) => {
|
||||
const areaDefinition = EXPLORATION_AREAS.find((definition) => {
|
||||
return definition.id === area.id;
|
||||
});
|
||||
return areaDefinition !== undefined
|
||||
&& newlyUnlockedZoneIds.has(areaDefinition.zoneId)
|
||||
&& area.status === "locked"
|
||||
? { ...area, status: "available" as const }
|
||||
: area;
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -289,6 +310,12 @@ interface GameContextValue {
|
||||
*/
|
||||
reload: ()=> Promise<void>;
|
||||
|
||||
/**
|
||||
* Reload state from the server without showing the loading screen (used
|
||||
* after prestige to avoid the visible flash/hang).
|
||||
*/
|
||||
reloadSilent: ()=> Promise<void>;
|
||||
|
||||
/**
|
||||
* Unix timestamp of the last successful cloud save (null until first save response).
|
||||
*/
|
||||
@@ -697,6 +724,10 @@ export const GameProvider = ({
|
||||
|
||||
/* No-op placeholder */
|
||||
});
|
||||
const reloadSilentReference = useRef<()=> Promise<void>>(async() => {
|
||||
|
||||
/* No-op placeholder */
|
||||
});
|
||||
const [ schemaOutdated, setSchemaOutdated ] = useState(false);
|
||||
const [ saveSchemaVersion, setSaveSchemaVersion ] = useState(0);
|
||||
const [ currentSchemaVersion, setCurrentSchemaVersion ] = useState(0);
|
||||
@@ -784,6 +815,32 @@ export const GameProvider = ({
|
||||
|
||||
reloadReference.current = reload;
|
||||
|
||||
const reloadSilent = useCallback(async() => {
|
||||
setError(null);
|
||||
try {
|
||||
const data = await loadGame();
|
||||
setState(data.state);
|
||||
setLastSavedAt(data.state.player.lastSavedAt);
|
||||
if (data.signature !== undefined) {
|
||||
signatureReference.current = data.signature;
|
||||
localStorage.setItem("elysium_save_signature", data.signature);
|
||||
}
|
||||
setLoginStreak(data.loginStreak);
|
||||
setSchemaOutdated(data.schemaOutdated);
|
||||
setSaveSchemaVersion(data.state.schemaVersion ?? 0);
|
||||
setCurrentSchemaVersion(data.currentSchemaVersion);
|
||||
setInGuild(data.inGuild);
|
||||
} catch (error_: unknown) {
|
||||
setError(
|
||||
error_ instanceof Error
|
||||
? error_.message
|
||||
: "Failed to load game",
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
reloadSilentReference.current = reloadSilent;
|
||||
|
||||
useEffect(() => {
|
||||
enableSoundsReference.current = enableSounds;
|
||||
}, [ enableSounds ]);
|
||||
@@ -1294,7 +1351,7 @@ export const GameProvider = ({
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("⭐ Prestige!", "You have ascended!");
|
||||
}
|
||||
await reloadReference.current();
|
||||
await reloadSilentReference.current();
|
||||
}).
|
||||
catch(() => {
|
||||
|
||||
@@ -1810,7 +1867,18 @@ export const GameProvider = ({
|
||||
|
||||
const collectExploration = useCallback(
|
||||
async(areaId: string): Promise<ExploreCollectResponse> => {
|
||||
isSyncingReference.current = true;
|
||||
const result = await collectExplorationApi({ areaId });
|
||||
|
||||
/*
|
||||
* Collect mutates server state outside the normal save flow — clear the
|
||||
* stale HMAC signature and reset the timer so the next auto-save fires
|
||||
* after React has re-rendered with the new materials in stateReference.
|
||||
*/
|
||||
signatureReference.current = null;
|
||||
localStorage.removeItem("elysium_save_signature");
|
||||
lastSaveReference.current = Date.now();
|
||||
isSyncingReference.current = false;
|
||||
setState((previous) => {
|
||||
if (previous?.exploration === undefined) {
|
||||
return previous;
|
||||
@@ -2341,6 +2409,7 @@ export const GameProvider = ({
|
||||
offlineEssence,
|
||||
offlineGold,
|
||||
reload,
|
||||
reloadSilent,
|
||||
resetProgress,
|
||||
saveSchemaVersion,
|
||||
schemaOutdated,
|
||||
|
||||
+168
-1
@@ -21,6 +21,7 @@ import {
|
||||
getActiveCompanionBonus,
|
||||
} from "@elysium/types";
|
||||
import { EQUIPMENT_SETS } from "../data/equipmentSets.js";
|
||||
import { EXPLORATION_AREAS } from "../data/explorations.js";
|
||||
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
||||
|
||||
/**
|
||||
@@ -243,10 +244,129 @@ export const computeEssencePerSecond = (state: GameState): number => {
|
||||
return essencePerSecond;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the effective per-unit stats for a single adventurer type,
|
||||
* applying all active multipliers (upgrades, prestige, equipment, echo,
|
||||
* crafted, companion). The returned values represent what a single
|
||||
* adventurer of this type currently contributes per second, matching the
|
||||
* per-unit contribution used by computeGoldPerSecond and
|
||||
* computeEssencePerSecond.
|
||||
* @param state - The current game state.
|
||||
* @param adventurerId - The ID of the adventurer to compute stats for.
|
||||
* @returns Effective per-unit goldPerSecond, essencePerSecond, and combatPower.
|
||||
*/
|
||||
export const computeEffectiveAdventurerStats = (
|
||||
state: GameState,
|
||||
adventurerId: string,
|
||||
): { combatPower: number; essencePerSecond: number; goldPerSecond: number } => {
|
||||
const adventurer = state.adventurers.find((a) => {
|
||||
return a.id === adventurerId;
|
||||
});
|
||||
|
||||
/* V8 ignore next 3 -- @preserve */
|
||||
if (adventurer === undefined) {
|
||||
return { combatPower: 0, essencePerSecond: 0, goldPerSecond: 0 };
|
||||
}
|
||||
|
||||
const upgradeMultiplier = state.upgrades.
|
||||
filter((upgrade) => {
|
||||
const isGlobal = upgrade.target === "global";
|
||||
const isThisAdventurer
|
||||
= upgrade.target === "adventurer"
|
||||
&& upgrade.adventurerId === adventurerId;
|
||||
return upgrade.purchased && (isGlobal || isThisAdventurer);
|
||||
}).
|
||||
reduce((mult, upgrade) => {
|
||||
return mult * upgrade.multiplier;
|
||||
}, 1);
|
||||
|
||||
const equippedItems = state.equipment.filter((item) => {
|
||||
return item.equipped;
|
||||
});
|
||||
const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => {
|
||||
return mult * (item.bonus.goldMultiplier ?? 1);
|
||||
}, 1);
|
||||
const equipmentCombatMultiplier = equippedItems.reduce((mult, item) => {
|
||||
return mult * (item.bonus.combatMultiplier ?? 1);
|
||||
}, 1);
|
||||
const equippedItemIds = equippedItems.map((item) => {
|
||||
return item.id;
|
||||
});
|
||||
const setBonuses = computeSetBonuses(equippedItemIds, EQUIPMENT_SETS);
|
||||
|
||||
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
||||
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
||||
// eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear
|
||||
const prestigeCombatMultiplier = 1 + state.prestige.count * 0.1;
|
||||
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
|
||||
const craftedGoldMultiplier
|
||||
= state.exploration?.craftedGoldMultiplier ?? 1;
|
||||
const craftedEssenceMultiplier
|
||||
= state.exploration?.craftedEssenceMultiplier ?? 1;
|
||||
const craftedCombatMultiplier
|
||||
= state.exploration?.craftedCombatMultiplier ?? 1;
|
||||
|
||||
const companionBonus = getActiveCompanionBonus(
|
||||
state.companions?.activeCompanionId,
|
||||
state.companions?.unlockedCompanionIds ?? [],
|
||||
);
|
||||
const companionGoldMult
|
||||
= companionBonus?.type === "passiveGold"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
const companionEssenceMult
|
||||
= companionBonus?.type === "essenceIncome"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
const companionCombatMult
|
||||
= companionBonus?.type === "bossDamage"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
|
||||
const goldPerSecond
|
||||
= adventurer.goldPerSecond
|
||||
* upgradeMultiplier
|
||||
* state.prestige.productionMultiplier
|
||||
* runestonesIncome
|
||||
* echoIncome
|
||||
* equipmentGoldMultiplier
|
||||
* setBonuses.goldMultiplier
|
||||
* craftedGoldMultiplier
|
||||
* companionGoldMult;
|
||||
|
||||
const essencePerSecond
|
||||
= adventurer.essencePerSecond
|
||||
* upgradeMultiplier
|
||||
* state.prestige.productionMultiplier
|
||||
* runestonesEssence
|
||||
* craftedEssenceMultiplier
|
||||
* companionEssenceMult;
|
||||
|
||||
const combatPower
|
||||
= adventurer.combatPower
|
||||
* upgradeMultiplier
|
||||
* prestigeCombatMultiplier
|
||||
* equipmentCombatMultiplier
|
||||
* setBonuses.combatMultiplier
|
||||
* echoCombatMultiplier
|
||||
* craftedCombatMultiplier
|
||||
* companionCombatMult;
|
||||
|
||||
return { combatPower, essencePerSecond, goldPerSecond };
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the party's total combat power, applying all active multipliers
|
||||
* (upgrades, prestige, equipment, set bonuses, echo, crafted, companion).
|
||||
* This mirrors the server-side calculatePartyStats in boss.ts.
|
||||
* This mirrors the server-side calculatePartyStats in boss.ts and is the
|
||||
* single source of truth for all combat-power checks in the client:
|
||||
* - Displayed as "Combat Power" in the resource bar
|
||||
* - Displayed as "Party DPS" in the boss panel
|
||||
* - Used to gate quest availability
|
||||
* Note: the active companion's bossDamage bonus is intentionally included
|
||||
* here, as it applies to the full combat power calculation (boss fights and
|
||||
* quest gating alike), matching the server-side behaviour.
|
||||
* @param state - The current game state.
|
||||
* @returns The total party combat power.
|
||||
*/
|
||||
@@ -327,6 +447,36 @@ export const computePartyCombatPower = (state: GameState): number => {
|
||||
* companionCombatMult;
|
||||
};
|
||||
|
||||
const basePrestigeThreshold = 1_000_000;
|
||||
const runestonesPerPrestigeLevelClient = 15;
|
||||
const maxBaseRunestones = 200;
|
||||
|
||||
/**
|
||||
* Computes the projected runestone reward if the player were to prestige right now.
|
||||
* Mirrors the server-side calculateRunestones formula exactly.
|
||||
* @param state - The current game state.
|
||||
* @returns The number of runestones the player would earn from a prestige now.
|
||||
*/
|
||||
export const computeProjectedRunestones = (state: GameState): number => {
|
||||
const { count, purchasedUpgradeIds } = state.prestige;
|
||||
const threshold = basePrestigeThreshold * Math.pow(count + 1, 2);
|
||||
const base = Math.min(
|
||||
Math.floor(Math.cbrt(state.player.totalGoldEarned / threshold))
|
||||
* runestonesPerPrestigeLevelClient,
|
||||
maxBaseRunestones,
|
||||
);
|
||||
const gain1Mult = purchasedUpgradeIds.includes("runestone_gain_1")
|
||||
? 1.25
|
||||
: 1;
|
||||
const gain2Mult = purchasedUpgradeIds.includes("runestone_gain_2")
|
||||
? 1.5
|
||||
: 1;
|
||||
const runestoneMult = gain1Mult * gain2Mult;
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- optional chained game state field */
|
||||
const echoMult: number = state.transcendence?.echoRunestoneMultiplier ?? 1;
|
||||
return Math.floor(base * runestoneMult * echoMult);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure function — applies one game tick to the state.
|
||||
* DeltaSeconds: time elapsed since last tick.
|
||||
@@ -634,6 +784,23 @@ export const applyTick = (
|
||||
...updatedDailyChallenges === undefined
|
||||
? {}
|
||||
: { dailyChallenges: updatedDailyChallenges },
|
||||
...newlyUnlockedZoneIds.size === 0 || state.exploration === undefined
|
||||
? {}
|
||||
: {
|
||||
exploration: {
|
||||
...state.exploration,
|
||||
areas: state.exploration.areas.map((area) => {
|
||||
const areaDefinition = EXPLORATION_AREAS.find((definition) => {
|
||||
return definition.id === area.id;
|
||||
});
|
||||
return areaDefinition !== undefined
|
||||
&& newlyUnlockedZoneIds.has(areaDefinition.zoneId)
|
||||
&& area.status === "locked"
|
||||
? { ...area, status: "available" as const }
|
||||
: area;
|
||||
}),
|
||||
},
|
||||
},
|
||||
adventurers: updatedAdventurers,
|
||||
bosses: updatedBosses,
|
||||
equipment: updatedEquipmentReference,
|
||||
|
||||
@@ -48,11 +48,17 @@ interface ProfileSettings {
|
||||
* Whether browser system notifications are enabled.
|
||||
*/
|
||||
enableNotifications: boolean;
|
||||
|
||||
/**
|
||||
* Whether prestige milestones are announced in the Discord server.
|
||||
*/
|
||||
enablePrestigeAnnouncements: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
||||
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
||||
enableNotifications: false,
|
||||
enablePrestigeAnnouncements: true,
|
||||
enableSounds: false,
|
||||
numberFormat: "suffix",
|
||||
showAchievementsUnlocked: true,
|
||||
|
||||
Reference in New Issue
Block a user