generated from nhcarrigan/template
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
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,
|
combatPower: 3,
|
||||||
count: 0,
|
count: 0,
|
||||||
essencePerSecond: 0,
|
essencePerSecond: 0,
|
||||||
goldPerSecond: 0.5,
|
goldPerSecond: 0.7,
|
||||||
id: "militia",
|
id: "militia",
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "Militia",
|
name: "Militia",
|
||||||
@@ -129,7 +129,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
|||||||
unlocked: false,
|
unlocked: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseCost: 2_600_000_000,
|
baseCost: 2_850_000_000,
|
||||||
class: "mage",
|
class: "mage",
|
||||||
combatPower: 13_000,
|
combatPower: 13_000,
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -141,7 +141,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
|||||||
unlocked: false,
|
unlocked: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseCost: 11_000_000_000,
|
baseCost: 13_500_000_000,
|
||||||
class: "rogue",
|
class: "rogue",
|
||||||
combatPower: 28_000,
|
combatPower: 28_000,
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -153,7 +153,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
|||||||
unlocked: false,
|
unlocked: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseCost: 47_000_000_000,
|
baseCost: 64_000_000_000,
|
||||||
class: "paladin",
|
class: "paladin",
|
||||||
combatPower: 60_000,
|
combatPower: 60_000,
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -165,7 +165,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
|||||||
unlocked: false,
|
unlocked: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseCost: 200_000_000_000,
|
baseCost: 300_000_000_000,
|
||||||
class: "rogue",
|
class: "rogue",
|
||||||
combatPower: 130_000,
|
combatPower: 130_000,
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -177,7 +177,7 @@ export const defaultAdventurers: Array<Adventurer> = [
|
|||||||
unlocked: false,
|
unlocked: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseCost: 1_400_000_000_000,
|
baseCost: 1_800_000_000_000,
|
||||||
class: "paladin",
|
class: "paladin",
|
||||||
combatPower: 400_000,
|
combatPower: 400_000,
|
||||||
count: 0,
|
count: 0,
|
||||||
|
|||||||
+70
-70
@@ -122,7 +122,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
// ── Shadow Marshes ────────────────────────────────────────────────────────
|
// ── Shadow Marshes ────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
bountyRunestones: 20,
|
bountyRunestones: 20,
|
||||||
crystalReward: 700,
|
crystalReward: 1500,
|
||||||
currentHp: 6_000_000,
|
currentHp: 6_000_000,
|
||||||
damagePerSecond: 1200,
|
damagePerSecond: 1200,
|
||||||
description:
|
description:
|
||||||
@@ -140,7 +140,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
bountyRunestones: 25,
|
bountyRunestones: 25,
|
||||||
crystalReward: 1500,
|
crystalReward: 3000,
|
||||||
currentHp: 12_000_000,
|
currentHp: 12_000_000,
|
||||||
damagePerSecond: 2400,
|
damagePerSecond: 2400,
|
||||||
description:
|
description:
|
||||||
@@ -158,7 +158,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
bountyRunestones: 30,
|
bountyRunestones: 30,
|
||||||
crystalReward: 3000,
|
crystalReward: 6000,
|
||||||
currentHp: 20_000_000,
|
currentHp: 20_000_000,
|
||||||
damagePerSecond: 4000,
|
damagePerSecond: 4000,
|
||||||
description:
|
description:
|
||||||
@@ -226,7 +226,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
name: "The Void Titan",
|
name: "The Void Titan",
|
||||||
prestigeRequirement: 0,
|
prestigeRequirement: 0,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "dark_templar_1" ],
|
||||||
zoneId: "frozen_peaks",
|
zoneId: "frozen_peaks",
|
||||||
},
|
},
|
||||||
// ── Volcanic Depths ───────────────────────────────────────────────────────
|
// ── Volcanic Depths ───────────────────────────────────────────────────────
|
||||||
@@ -353,7 +353,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "seraph_guardian",
|
id: "seraph_guardian",
|
||||||
maxHp: 500_000_000,
|
maxHp: 500_000_000,
|
||||||
name: "The Seraph Guardian",
|
name: "The Seraph Guardian",
|
||||||
prestigeRequirement: 6,
|
prestigeRequirement: 1,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "click_4" ],
|
upgradeRewards: [ "click_4" ],
|
||||||
zoneId: "celestial_reaches",
|
zoneId: "celestial_reaches",
|
||||||
@@ -371,7 +371,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "fallen_archangel",
|
id: "fallen_archangel",
|
||||||
maxHp: 2_000_000_000,
|
maxHp: 2_000_000_000,
|
||||||
name: "The Fallen Archangel",
|
name: "The Fallen Archangel",
|
||||||
prestigeRequirement: 7,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "celestial_reaches",
|
zoneId: "celestial_reaches",
|
||||||
@@ -389,7 +389,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "divine_judge",
|
id: "divine_judge",
|
||||||
maxHp: 8_000_000_000,
|
maxHp: 8_000_000_000,
|
||||||
name: "The Divine Judge",
|
name: "The Divine Judge",
|
||||||
prestigeRequirement: 8,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "divine_covenant" ],
|
upgradeRewards: [ "divine_covenant" ],
|
||||||
zoneId: "celestial_reaches",
|
zoneId: "celestial_reaches",
|
||||||
@@ -407,7 +407,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "celestial_titan",
|
id: "celestial_titan",
|
||||||
maxHp: 30_000_000_000,
|
maxHp: 30_000_000_000,
|
||||||
name: "The Celestial Titan",
|
name: "The Celestial Titan",
|
||||||
prestigeRequirement: 9,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "celestial_reaches",
|
zoneId: "celestial_reaches",
|
||||||
@@ -425,7 +425,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_first_light",
|
id: "the_first_light",
|
||||||
maxHp: 100_000_000_000,
|
maxHp: 100_000_000_000,
|
||||||
name: "The First Light",
|
name: "The First Light",
|
||||||
prestigeRequirement: 10,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "celestial_reaches",
|
zoneId: "celestial_reaches",
|
||||||
@@ -444,7 +444,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "depth_leviathan",
|
id: "depth_leviathan",
|
||||||
maxHp: 250_000_000_000,
|
maxHp: 250_000_000_000,
|
||||||
name: "The Depth Leviathan",
|
name: "The Depth Leviathan",
|
||||||
prestigeRequirement: 9,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "abyssal_trench",
|
zoneId: "abyssal_trench",
|
||||||
@@ -462,7 +462,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "kraken_elder",
|
id: "kraken_elder",
|
||||||
maxHp: 1_000_000_000_000,
|
maxHp: 1_000_000_000_000,
|
||||||
name: "The Elder Kraken",
|
name: "The Elder Kraken",
|
||||||
prestigeRequirement: 10,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "abyssal_pact" ],
|
upgradeRewards: [ "abyssal_pact" ],
|
||||||
zoneId: "abyssal_trench",
|
zoneId: "abyssal_trench",
|
||||||
@@ -480,7 +480,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "abyssal_colossus",
|
id: "abyssal_colossus",
|
||||||
maxHp: 4_000_000_000_000,
|
maxHp: 4_000_000_000_000,
|
||||||
name: "The Abyssal Colossus",
|
name: "The Abyssal Colossus",
|
||||||
prestigeRequirement: 11,
|
prestigeRequirement: 2,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "abyssal_trench",
|
zoneId: "abyssal_trench",
|
||||||
@@ -498,7 +498,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_deep_one",
|
id: "the_deep_one",
|
||||||
maxHp: 15_000_000_000_000,
|
maxHp: 15_000_000_000_000,
|
||||||
name: "The Deep One",
|
name: "The Deep One",
|
||||||
prestigeRequirement: 12,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "global_4" ],
|
upgradeRewards: [ "global_4" ],
|
||||||
zoneId: "abyssal_trench",
|
zoneId: "abyssal_trench",
|
||||||
@@ -516,7 +516,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "elder_abomination",
|
id: "elder_abomination",
|
||||||
maxHp: 50_000_000_000_000,
|
maxHp: 50_000_000_000_000,
|
||||||
name: "The Elder Abomination",
|
name: "The Elder Abomination",
|
||||||
prestigeRequirement: 13,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "abyssal_trench",
|
zoneId: "abyssal_trench",
|
||||||
@@ -535,7 +535,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "demon_prince",
|
id: "demon_prince",
|
||||||
maxHp: 120_000_000_000_000,
|
maxHp: 120_000_000_000_000,
|
||||||
name: "The Demon Prince",
|
name: "The Demon Prince",
|
||||||
prestigeRequirement: 12,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "infernal_court",
|
zoneId: "infernal_court",
|
||||||
@@ -553,7 +553,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "hellfire_titan",
|
id: "hellfire_titan",
|
||||||
maxHp: 500_000_000_000_000,
|
maxHp: 500_000_000_000_000,
|
||||||
name: "The Hellfire Titan",
|
name: "The Hellfire Titan",
|
||||||
prestigeRequirement: 13,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "celestial_mandate" ],
|
upgradeRewards: [ "celestial_mandate" ],
|
||||||
zoneId: "infernal_court",
|
zoneId: "infernal_court",
|
||||||
@@ -571,7 +571,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "lord_of_sin",
|
id: "lord_of_sin",
|
||||||
maxHp: 2_000_000_000_000_000,
|
maxHp: 2_000_000_000_000_000,
|
||||||
name: "The Lord of Sin",
|
name: "The Lord of Sin",
|
||||||
prestigeRequirement: 14,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "infernal_court",
|
zoneId: "infernal_court",
|
||||||
@@ -589,7 +589,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "infernal_sovereign",
|
id: "infernal_sovereign",
|
||||||
maxHp: 6_000_000_000_000_000,
|
maxHp: 6_000_000_000_000_000,
|
||||||
name: "The Infernal Sovereign",
|
name: "The Infernal Sovereign",
|
||||||
prestigeRequirement: 15,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "click_5" ],
|
upgradeRewards: [ "click_5" ],
|
||||||
zoneId: "infernal_court",
|
zoneId: "infernal_court",
|
||||||
@@ -607,7 +607,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_fallen",
|
id: "the_fallen",
|
||||||
maxHp: 8_000_000_000_000_000,
|
maxHp: 8_000_000_000_000_000,
|
||||||
name: "The Fallen",
|
name: "The Fallen",
|
||||||
prestigeRequirement: 16,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "infernal_court",
|
zoneId: "infernal_court",
|
||||||
@@ -626,9 +626,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "prism_golem",
|
id: "prism_golem",
|
||||||
maxHp: 2e16,
|
maxHp: 2e16,
|
||||||
name: "The Prism Golem",
|
name: "The Prism Golem",
|
||||||
prestigeRequirement: 15,
|
prestigeRequirement: 3,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "crystal_sage_1" ],
|
||||||
zoneId: "crystalline_spire",
|
zoneId: "crystalline_spire",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -644,7 +644,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "crystal_drake",
|
id: "crystal_drake",
|
||||||
maxHp: 8e16,
|
maxHp: 8e16,
|
||||||
name: "The Crystal Drake",
|
name: "The Crystal Drake",
|
||||||
prestigeRequirement: 16,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "void_ascendancy" ],
|
upgradeRewards: [ "void_ascendancy" ],
|
||||||
zoneId: "crystalline_spire",
|
zoneId: "crystalline_spire",
|
||||||
@@ -662,9 +662,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_faceted",
|
id: "the_faceted",
|
||||||
maxHp: 3e17,
|
maxHp: 3e17,
|
||||||
name: "The Faceted",
|
name: "The Faceted",
|
||||||
prestigeRequirement: 17,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "void_sentinel_1" ],
|
||||||
zoneId: "crystalline_spire",
|
zoneId: "crystalline_spire",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -680,9 +680,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "diamond_colossus",
|
id: "diamond_colossus",
|
||||||
maxHp: 1e18,
|
maxHp: 1e18,
|
||||||
name: "The Diamond Colossus",
|
name: "The Diamond Colossus",
|
||||||
prestigeRequirement: 18,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "eternal_champion_1" ],
|
||||||
zoneId: "crystalline_spire",
|
zoneId: "crystalline_spire",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -698,9 +698,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "crystal_sovereign",
|
id: "crystal_sovereign",
|
||||||
maxHp: 4e18,
|
maxHp: 4e18,
|
||||||
name: "The Crystal Sovereign",
|
name: "The Crystal Sovereign",
|
||||||
prestigeRequirement: 19,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "cosmos_knight_1" ],
|
||||||
zoneId: "crystalline_spire",
|
zoneId: "crystalline_spire",
|
||||||
},
|
},
|
||||||
// ── Void Sanctum ──────────────────────────────────────────────────────────
|
// ── Void Sanctum ──────────────────────────────────────────────────────────
|
||||||
@@ -717,9 +717,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "void_herald",
|
id: "void_herald",
|
||||||
maxHp: 1e19,
|
maxHp: 1e19,
|
||||||
name: "The Void Herald",
|
name: "The Void Herald",
|
||||||
prestigeRequirement: 18,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "seraph_knight_1" ],
|
||||||
zoneId: "void_sanctum",
|
zoneId: "void_sanctum",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -735,7 +735,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "eternal_shade",
|
id: "eternal_shade",
|
||||||
maxHp: 5e19,
|
maxHp: 5e19,
|
||||||
name: "The Eternal Shade",
|
name: "The Eternal Shade",
|
||||||
prestigeRequirement: 19,
|
prestigeRequirement: 4,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "divine_harmony" ],
|
upgradeRewards: [ "divine_harmony" ],
|
||||||
zoneId: "void_sanctum",
|
zoneId: "void_sanctum",
|
||||||
@@ -753,9 +753,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_unmaker",
|
id: "the_unmaker",
|
||||||
maxHp: 2e20,
|
maxHp: 2e20,
|
||||||
name: "The Unmaker",
|
name: "The Unmaker",
|
||||||
prestigeRequirement: 20,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "abyss_diver_1" ],
|
||||||
zoneId: "void_sanctum",
|
zoneId: "void_sanctum",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -771,7 +771,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "void_progenitor",
|
id: "void_progenitor",
|
||||||
maxHp: 8e20,
|
maxHp: 8e20,
|
||||||
name: "The Void Progenitor",
|
name: "The Void Progenitor",
|
||||||
prestigeRequirement: 21,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "void_sanctum",
|
zoneId: "void_sanctum",
|
||||||
@@ -789,9 +789,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "void_emperor",
|
id: "void_emperor",
|
||||||
maxHp: 3e21,
|
maxHp: 3e21,
|
||||||
name: "The Void Emperor",
|
name: "The Void Emperor",
|
||||||
prestigeRequirement: 22,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "infernal_warden_1" ],
|
||||||
zoneId: "void_sanctum",
|
zoneId: "void_sanctum",
|
||||||
},
|
},
|
||||||
// ── Eternal Throne ────────────────────────────────────────────────────────
|
// ── Eternal Throne ────────────────────────────────────────────────────────
|
||||||
@@ -808,9 +808,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "throne_warden",
|
id: "throne_warden",
|
||||||
maxHp: 1e22,
|
maxHp: 1e22,
|
||||||
name: "The Throne Warden",
|
name: "The Throne Warden",
|
||||||
prestigeRequirement: 21,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "infinity_ranger_1" ],
|
||||||
zoneId: "eternal_throne",
|
zoneId: "eternal_throne",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -826,7 +826,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "eternal_knight",
|
id: "eternal_knight",
|
||||||
maxHp: 5e22,
|
maxHp: 5e22,
|
||||||
name: "The Eternal Knight",
|
name: "The Eternal Knight",
|
||||||
prestigeRequirement: 22,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "infernal_fury" ],
|
upgradeRewards: [ "infernal_fury" ],
|
||||||
zoneId: "eternal_throne",
|
zoneId: "eternal_throne",
|
||||||
@@ -844,9 +844,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_undying",
|
id: "the_undying",
|
||||||
maxHp: 2e23,
|
maxHp: 2e23,
|
||||||
name: "The Undying",
|
name: "The Undying",
|
||||||
prestigeRequirement: 23,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "reality_warden_1" ],
|
||||||
zoneId: "eternal_throne",
|
zoneId: "eternal_throne",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -862,7 +862,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "apex_sovereign",
|
id: "apex_sovereign",
|
||||||
maxHp: 8e23,
|
maxHp: 8e23,
|
||||||
name: "The Apex Sovereign",
|
name: "The Apex Sovereign",
|
||||||
prestigeRequirement: 24,
|
prestigeRequirement: 5,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "eternal_throne",
|
zoneId: "eternal_throne",
|
||||||
@@ -880,7 +880,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_apex",
|
id: "the_apex",
|
||||||
maxHp: 3e24,
|
maxHp: 3e24,
|
||||||
name: "The Apex",
|
name: "The Apex",
|
||||||
prestigeRequirement: 25,
|
prestigeRequirement: 6,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "eternal_throne",
|
zoneId: "eternal_throne",
|
||||||
@@ -899,7 +899,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "chaos_wyrm",
|
id: "chaos_wyrm",
|
||||||
maxHp: 1e26,
|
maxHp: 1e26,
|
||||||
name: "The Chaos Wyrm",
|
name: "The Chaos Wyrm",
|
||||||
prestigeRequirement: 26,
|
prestigeRequirement: 6,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "primordial_chaos",
|
zoneId: "primordial_chaos",
|
||||||
@@ -917,7 +917,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "creation_engine",
|
id: "creation_engine",
|
||||||
maxHp: 5e27,
|
maxHp: 5e27,
|
||||||
name: "The Creation Engine",
|
name: "The Creation Engine",
|
||||||
prestigeRequirement: 27,
|
prestigeRequirement: 6,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "aether_weaver_1" ],
|
upgradeRewards: [ "aether_weaver_1" ],
|
||||||
zoneId: "primordial_chaos",
|
zoneId: "primordial_chaos",
|
||||||
@@ -935,7 +935,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "entropy_avatar",
|
id: "entropy_avatar",
|
||||||
maxHp: 2e29,
|
maxHp: 2e29,
|
||||||
name: "The Entropy Avatar",
|
name: "The Entropy Avatar",
|
||||||
prestigeRequirement: 29,
|
prestigeRequirement: 7,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "primordial_chaos",
|
zoneId: "primordial_chaos",
|
||||||
@@ -953,7 +953,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "primordial_titan",
|
id: "primordial_titan",
|
||||||
maxHp: 8e30,
|
maxHp: 8e30,
|
||||||
name: "The Primordial Titan",
|
name: "The Primordial Titan",
|
||||||
prestigeRequirement: 31,
|
prestigeRequirement: 7,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "primordial_chaos",
|
zoneId: "primordial_chaos",
|
||||||
@@ -972,7 +972,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "expanse_drifter",
|
id: "expanse_drifter",
|
||||||
maxHp: 3e33,
|
maxHp: 3e33,
|
||||||
name: "The Expanse Drifter",
|
name: "The Expanse Drifter",
|
||||||
prestigeRequirement: 33,
|
prestigeRequirement: 8,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "titan_warrior_1" ],
|
upgradeRewards: [ "titan_warrior_1" ],
|
||||||
zoneId: "infinite_expanse",
|
zoneId: "infinite_expanse",
|
||||||
@@ -990,9 +990,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "horizon_beast",
|
id: "horizon_beast",
|
||||||
maxHp: 1e37,
|
maxHp: 1e37,
|
||||||
name: "The Horizon Beast",
|
name: "The Horizon Beast",
|
||||||
prestigeRequirement: 35,
|
prestigeRequirement: 8,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "oblivion_paladin_1" ],
|
||||||
zoneId: "infinite_expanse",
|
zoneId: "infinite_expanse",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1008,7 +1008,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "infinity_construct",
|
id: "infinity_construct",
|
||||||
maxHp: 5e40,
|
maxHp: 5e40,
|
||||||
name: "The Infinity Construct",
|
name: "The Infinity Construct",
|
||||||
prestigeRequirement: 37,
|
prestigeRequirement: 8,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "infinite_expanse",
|
zoneId: "infinite_expanse",
|
||||||
@@ -1026,7 +1026,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "expanse_sovereign",
|
id: "expanse_sovereign",
|
||||||
maxHp: 2e44,
|
maxHp: 2e44,
|
||||||
name: "The Expanse Sovereign",
|
name: "The Expanse Sovereign",
|
||||||
prestigeRequirement: 39,
|
prestigeRequirement: 9,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "infinite_expanse",
|
zoneId: "infinite_expanse",
|
||||||
@@ -1045,7 +1045,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "forge_guardian",
|
id: "forge_guardian",
|
||||||
maxHp: 8e47,
|
maxHp: 8e47,
|
||||||
name: "The Forge Guardian",
|
name: "The Forge Guardian",
|
||||||
prestigeRequirement: 41,
|
prestigeRequirement: 9,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "nexus_sage_1" ],
|
upgradeRewards: [ "nexus_sage_1" ],
|
||||||
zoneId: "reality_forge",
|
zoneId: "reality_forge",
|
||||||
@@ -1063,7 +1063,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "reality_shaper",
|
id: "reality_shaper",
|
||||||
maxHp: 4e52,
|
maxHp: 4e52,
|
||||||
name: "The Reality Shaper",
|
name: "The Reality Shaper",
|
||||||
prestigeRequirement: 44,
|
prestigeRequirement: 10,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "reality_forge",
|
zoneId: "reality_forge",
|
||||||
@@ -1081,7 +1081,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "creation_prime",
|
id: "creation_prime",
|
||||||
maxHp: 2e57,
|
maxHp: 2e57,
|
||||||
name: "The Creation Prime",
|
name: "The Creation Prime",
|
||||||
prestigeRequirement: 47,
|
prestigeRequirement: 11,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "reality_forge",
|
zoneId: "reality_forge",
|
||||||
@@ -1099,7 +1099,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "reality_architect",
|
id: "reality_architect",
|
||||||
maxHp: 8e61,
|
maxHp: 8e61,
|
||||||
name: "The Reality Architect",
|
name: "The Reality Architect",
|
||||||
prestigeRequirement: 49,
|
prestigeRequirement: 11,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "reality_forge",
|
zoneId: "reality_forge",
|
||||||
@@ -1118,7 +1118,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "storm_colossus",
|
id: "storm_colossus",
|
||||||
maxHp: 4e65,
|
maxHp: 4e65,
|
||||||
name: "The Storm Colossus",
|
name: "The Storm Colossus",
|
||||||
prestigeRequirement: 51,
|
prestigeRequirement: 12,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
@@ -1136,7 +1136,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "force_prime",
|
id: "force_prime",
|
||||||
maxHp: 2e71,
|
maxHp: 2e71,
|
||||||
name: "The Force Prime",
|
name: "The Force Prime",
|
||||||
prestigeRequirement: 54,
|
prestigeRequirement: 12,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
@@ -1154,9 +1154,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "maelstrom_god",
|
id: "maelstrom_god",
|
||||||
maxHp: 1e77,
|
maxHp: 1e77,
|
||||||
name: "The Maelstrom God",
|
name: "The Maelstrom God",
|
||||||
prestigeRequirement: 57,
|
prestigeRequirement: 13,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "transcendent_rogue_1" ],
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1172,7 +1172,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "cosmic_annihilator",
|
id: "cosmic_annihilator",
|
||||||
maxHp: 5e82,
|
maxHp: 5e82,
|
||||||
name: "The Cosmic Annihilator",
|
name: "The Cosmic Annihilator",
|
||||||
prestigeRequirement: 59,
|
prestigeRequirement: 13,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
@@ -1191,7 +1191,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "ancient_sentinel",
|
id: "ancient_sentinel",
|
||||||
maxHp: 2e88,
|
maxHp: 2e88,
|
||||||
name: "The Ancient Sentinel",
|
name: "The Ancient Sentinel",
|
||||||
prestigeRequirement: 61,
|
prestigeRequirement: 14,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "astral_sovereign_1" ],
|
upgradeRewards: [ "astral_sovereign_1" ],
|
||||||
zoneId: "primeval_sanctum",
|
zoneId: "primeval_sanctum",
|
||||||
@@ -1209,7 +1209,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "time_elder",
|
id: "time_elder",
|
||||||
maxHp: 1e95,
|
maxHp: 1e95,
|
||||||
name: "The Time Elder",
|
name: "The Time Elder",
|
||||||
prestigeRequirement: 65,
|
prestigeRequirement: 15,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "primeval_sanctum",
|
zoneId: "primeval_sanctum",
|
||||||
@@ -1227,7 +1227,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "origin_beast",
|
id: "origin_beast",
|
||||||
maxHp: 8e101,
|
maxHp: 8e101,
|
||||||
name: "The Origin Beast",
|
name: "The Origin Beast",
|
||||||
prestigeRequirement: 69,
|
prestigeRequirement: 16,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "primeval_sanctum",
|
zoneId: "primeval_sanctum",
|
||||||
@@ -1245,7 +1245,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "primeval_god",
|
id: "primeval_god",
|
||||||
maxHp: 5e108,
|
maxHp: 5e108,
|
||||||
name: "The Primeval God",
|
name: "The Primeval God",
|
||||||
prestigeRequirement: 74,
|
prestigeRequirement: 17,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "primeval_sanctum",
|
zoneId: "primeval_sanctum",
|
||||||
@@ -1264,7 +1264,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "absolute_herald",
|
id: "absolute_herald",
|
||||||
maxHp: 2e116,
|
maxHp: 2e116,
|
||||||
name: "The Absolute Herald",
|
name: "The Absolute Herald",
|
||||||
prestigeRequirement: 76,
|
prestigeRequirement: 17,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [ "primordial_mage_1" ],
|
upgradeRewards: [ "primordial_mage_1" ],
|
||||||
zoneId: "the_absolute",
|
zoneId: "the_absolute",
|
||||||
@@ -1282,7 +1282,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "void_convergence",
|
id: "void_convergence",
|
||||||
maxHp: 1e125,
|
maxHp: 1e125,
|
||||||
name: "The Void Convergence",
|
name: "The Void Convergence",
|
||||||
prestigeRequirement: 79,
|
prestigeRequirement: 18,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "the_absolute",
|
zoneId: "the_absolute",
|
||||||
@@ -1300,9 +1300,9 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "eternal_end",
|
id: "eternal_end",
|
||||||
maxHp: 5e134,
|
maxHp: 5e134,
|
||||||
name: "The Eternal End",
|
name: "The Eternal End",
|
||||||
prestigeRequirement: 83,
|
prestigeRequirement: 19,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [ "omniversal_champion_1" ],
|
||||||
zoneId: "the_absolute",
|
zoneId: "the_absolute",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1318,7 +1318,7 @@ export const defaultBosses: Array<Boss> = [
|
|||||||
id: "the_absolute_one",
|
id: "the_absolute_one",
|
||||||
maxHp: 2e145,
|
maxHp: 2e145,
|
||||||
name: "The Absolute One",
|
name: "The Absolute One",
|
||||||
prestigeRequirement: 88,
|
prestigeRequirement: 20,
|
||||||
status: "locked",
|
status: "locked",
|
||||||
upgradeRewards: [],
|
upgradeRewards: [],
|
||||||
zoneId: "the_absolute",
|
zoneId: "the_absolute",
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const defaultEquipment: Array<Equipment> = [
|
|||||||
type: "trinket",
|
type: "trinket",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { clickMultiplier: 1.55, goldMultiplier: 1.1 },
|
bonus: { clickMultiplier: 1.65, goldMultiplier: 1.2 },
|
||||||
description:
|
description:
|
||||||
"A fragment of the Mud Kraken's crystallised essence. Focuses raw power into devastating strikes.",
|
"A fragment of the Mud Kraken's crystallised essence. Focuses raw power into devastating strikes.",
|
||||||
equipped: false,
|
equipped: false,
|
||||||
@@ -305,9 +305,9 @@ export const defaultEquipment: Array<Equipment> = [
|
|||||||
type: "trinket",
|
type: "trinket",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { clickMultiplier: 2.25, goldMultiplier: 1.25 },
|
bonus: { clickMultiplier: 2.5, goldMultiplier: 1.4 },
|
||||||
description:
|
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,
|
equipped: false,
|
||||||
id: "philosophers_stone",
|
id: "philosophers_stone",
|
||||||
name: "Philosopher's Stone",
|
name: "Philosopher's Stone",
|
||||||
@@ -697,7 +697,7 @@ export const defaultEquipment: Array<Equipment> = [
|
|||||||
},
|
},
|
||||||
// ── Purchasable endgame sinks ─────────────────────────────────────────────
|
// ── Purchasable endgame sinks ─────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
bonus: { clickMultiplier: 3 },
|
bonus: { clickMultiplier: 4.25 },
|
||||||
cost: { crystals: 0, essence: 20_000_000, gold: 0 },
|
cost: { crystals: 0, essence: 20_000_000, gold: 0 },
|
||||||
description:
|
description:
|
||||||
"A lens of compressed celestial light that sharpens every strike with divine precision.",
|
"A lens of compressed celestial light that sharpens every strike with divine precision.",
|
||||||
@@ -721,7 +721,7 @@ export const defaultEquipment: Array<Equipment> = [
|
|||||||
type: "armour",
|
type: "armour",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { combatMultiplier: 7 },
|
bonus: { combatMultiplier: 10.5 },
|
||||||
cost: { crystals: 0, essence: 100_000_000, gold: 0 },
|
cost: { crystals: 0, essence: 100_000_000, gold: 0 },
|
||||||
description:
|
description:
|
||||||
"A weapon that channels void energy — the absence of resistance makes every strike devastating.",
|
"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",
|
type: "trinket",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { goldMultiplier: 4.75 },
|
bonus: { goldMultiplier: 7.5 },
|
||||||
cost: { crystals: 20_000_000, essence: 0, gold: 0 },
|
cost: { crystals: 20_000_000, essence: 0, gold: 0 },
|
||||||
description:
|
description:
|
||||||
"Armour structured around a crystalline lattice of optimal income calculations. Every gold piece finds you faster.",
|
"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: 2000, type: "gold" },
|
||||||
{ amount: 5, type: "essence" },
|
{ amount: 5, type: "essence" },
|
||||||
{ targetId: "peasant_1", type: "upgrade" },
|
{ targetId: "peasant_1", type: "upgrade" },
|
||||||
|
{ targetId: "apprentice_1", type: "upgrade" },
|
||||||
{ targetId: "apprentice", type: "adventurer" },
|
{ targetId: "apprentice", type: "adventurer" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
@@ -50,6 +51,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 10, type: "crystals" },
|
{ amount: 10, type: "crystals" },
|
||||||
{ targetId: "global_1", type: "upgrade" },
|
{ targetId: "global_1", type: "upgrade" },
|
||||||
|
{ targetId: "militia_1", type: "upgrade" },
|
||||||
{ targetId: "scout", type: "adventurer" },
|
{ targetId: "scout", type: "adventurer" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
@@ -82,7 +84,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 15_000, type: "gold" },
|
{ amount: 15_000, type: "gold" },
|
||||||
{ amount: 20, type: "essence" },
|
{ amount: 20, type: "essence" },
|
||||||
{ targetId: "militia_1", type: "upgrade" },
|
|
||||||
{ targetId: "acolyte_1", type: "upgrade" },
|
{ targetId: "acolyte_1", type: "upgrade" },
|
||||||
{ targetId: "ranger", type: "adventurer" },
|
{ targetId: "ranger", type: "adventurer" },
|
||||||
],
|
],
|
||||||
@@ -117,7 +118,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 300, type: "essence" },
|
{ amount: 300, type: "essence" },
|
||||||
{ amount: 30, type: "crystals" },
|
{ amount: 30, type: "crystals" },
|
||||||
{ targetId: "apprentice_1", type: "upgrade" },
|
|
||||||
{ targetId: "archmage", type: "adventurer" },
|
{ targetId: "archmage", type: "adventurer" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
@@ -153,6 +153,23 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 5_000_000, type: "gold" },
|
{ amount: 5_000_000, type: "gold" },
|
||||||
{ amount: 100, type: "crystals" },
|
{ amount: 100, type: "crystals" },
|
||||||
{ targetId: "global_3", type: "upgrade" },
|
{ 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",
|
status: "locked",
|
||||||
zoneId: "frozen_peaks",
|
zoneId: "frozen_peaks",
|
||||||
@@ -164,7 +181,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
durationSeconds: 3 * 60 * 60,
|
durationSeconds: 3 * 60 * 60,
|
||||||
id: "ice_caves",
|
id: "ice_caves",
|
||||||
name: "The Ice Caves",
|
name: "The Ice Caves",
|
||||||
prerequisiteIds: [ "frozen_wastes" ],
|
prerequisiteIds: [ "glacier_tomb" ],
|
||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 5000, type: "essence" },
|
{ amount: 5000, type: "essence" },
|
||||||
{ amount: 200, type: "crystals" },
|
{ amount: 200, type: "crystals" },
|
||||||
@@ -188,9 +205,25 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "frozen_peaks",
|
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 ────────────────────────────────────────────────────────
|
// ── Shadow Marshes ────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
combatPowerRequired: 5_000_000,
|
combatPowerRequired: 2_000_000,
|
||||||
description:
|
description:
|
||||||
"A cursed lake shrouded in permanent twilight. Strange energies pulse beneath its surface.",
|
"A cursed lake shrouded in permanent twilight. Strange energies pulse beneath its surface.",
|
||||||
durationSeconds: 45 * 60,
|
durationSeconds: 45 * 60,
|
||||||
@@ -198,7 +231,10 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "The Shadow Mere",
|
name: "The Shadow Mere",
|
||||||
prerequisiteIds: [],
|
prerequisiteIds: [],
|
||||||
rewards: [
|
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",
|
status: "locked",
|
||||||
zoneId: "shadow_marshes",
|
zoneId: "shadow_marshes",
|
||||||
@@ -212,7 +248,9 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "The Witch Coven",
|
name: "The Witch Coven",
|
||||||
prerequisiteIds: [ "shadow_mere" ],
|
prerequisiteIds: [ "shadow_mere" ],
|
||||||
rewards: [
|
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" },
|
{ targetId: "shadow_assassin", type: "adventurer" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
@@ -230,8 +268,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 2_000_000, type: "gold" },
|
{ amount: 2_000_000, type: "gold" },
|
||||||
{ amount: 1500, type: "essence" },
|
{ amount: 1500, type: "essence" },
|
||||||
{ amount: 75, type: "crystals" },
|
{ amount: 75, type: "crystals" },
|
||||||
{ targetId: "knight_1", type: "upgrade" },
|
|
||||||
{ targetId: "peasant_2", type: "upgrade" },
|
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "shadow_marshes",
|
zoneId: "shadow_marshes",
|
||||||
@@ -245,9 +281,9 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "The Plague Ruins",
|
name: "The Plague Ruins",
|
||||||
prerequisiteIds: [ "sunken_temple" ],
|
prerequisiteIds: [ "sunken_temple" ],
|
||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 8_000_000, type: "gold" },
|
{ amount: 100_000_000, type: "gold" },
|
||||||
{ amount: 2000, type: "essence" },
|
{ amount: 30_000, type: "essence" },
|
||||||
{ amount: 150, type: "crystals" },
|
{ amount: 500, type: "crystals" },
|
||||||
{ targetId: "dark_templar", type: "adventurer" },
|
{ targetId: "dark_templar", type: "adventurer" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
@@ -282,7 +318,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 40_000_000, type: "gold" },
|
{ amount: 40_000_000, type: "gold" },
|
||||||
{ amount: 12_000, type: "essence" },
|
{ amount: 12_000, type: "essence" },
|
||||||
{ amount: 300, type: "crystals" },
|
{ amount: 300, type: "crystals" },
|
||||||
{ targetId: "peasant_3", type: "upgrade" },
|
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "volcanic_depths",
|
zoneId: "volcanic_depths",
|
||||||
@@ -329,8 +364,9 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "Void Rift",
|
name: "Void Rift",
|
||||||
prerequisiteIds: [],
|
prerequisiteIds: [],
|
||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 500, type: "crystals" },
|
{ amount: 2_000_000_000, type: "gold" },
|
||||||
{ amount: 5000, type: "essence" },
|
{ amount: 300_000, type: "essence" },
|
||||||
|
{ amount: 1000, type: "crystals" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "astral_void",
|
zoneId: "astral_void",
|
||||||
@@ -344,9 +380,9 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "The Star Graveyard",
|
name: "The Star Graveyard",
|
||||||
prerequisiteIds: [ "void_rift" ],
|
prerequisiteIds: [ "void_rift" ],
|
||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 1_000_000_000, type: "gold" },
|
{ amount: 8_000_000_000, type: "gold" },
|
||||||
{ amount: 100_000, type: "essence" },
|
{ amount: 800_000, type: "essence" },
|
||||||
{ amount: 1000, type: "crystals" },
|
{ amount: 3000, type: "crystals" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "astral_void",
|
zoneId: "astral_void",
|
||||||
@@ -360,8 +396,9 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "Between Worlds",
|
name: "Between Worlds",
|
||||||
prerequisiteIds: [ "star_graveyard" ],
|
prerequisiteIds: [ "star_graveyard" ],
|
||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 250_000, type: "essence" },
|
{ amount: 25_000_000_000, type: "gold" },
|
||||||
{ amount: 2000, type: "crystals" },
|
{ amount: 2_000_000, type: "essence" },
|
||||||
|
{ amount: 8000, type: "crystals" },
|
||||||
{ targetId: "divine_champion", type: "adventurer" },
|
{ targetId: "divine_champion", type: "adventurer" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
@@ -376,9 +413,9 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
name: "The End of All Things",
|
name: "The End of All Things",
|
||||||
prerequisiteIds: [ "between_worlds" ],
|
prerequisiteIds: [ "between_worlds" ],
|
||||||
rewards: [
|
rewards: [
|
||||||
{ amount: 10_000_000_000, type: "gold" },
|
{ amount: 80_000_000_000, type: "gold" },
|
||||||
{ amount: 1_000_000, type: "essence" },
|
{ amount: 5_000_000, type: "essence" },
|
||||||
{ amount: 10_000, type: "crystals" },
|
{ amount: 20_000, type: "crystals" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "astral_void",
|
zoneId: "astral_void",
|
||||||
@@ -1148,6 +1185,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 8e39, type: "gold" },
|
{ amount: 8e39, type: "gold" },
|
||||||
{ amount: 2.5e36, type: "essence" },
|
{ amount: 2.5e36, type: "essence" },
|
||||||
{ amount: 5e32, type: "crystals" },
|
{ amount: 5e32, type: "crystals" },
|
||||||
|
{ targetId: "cosmos_knight_1", type: "upgrade" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "infinite_expanse",
|
zoneId: "infinite_expanse",
|
||||||
@@ -1230,7 +1268,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 2.5e49, type: "gold" },
|
{ amount: 2.5e49, type: "gold" },
|
||||||
{ amount: 8e45, type: "essence" },
|
{ amount: 8e45, type: "essence" },
|
||||||
{ amount: 5e41, type: "crystals" },
|
{ amount: 5e41, type: "crystals" },
|
||||||
{ targetId: "cosmos_knight_1", type: "upgrade" },
|
{ targetId: "primordial_mage_1", type: "upgrade" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "reality_forge",
|
zoneId: "reality_forge",
|
||||||
@@ -1263,6 +1301,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 6e52, type: "gold" },
|
{ amount: 6e52, type: "gold" },
|
||||||
{ amount: 2e49, type: "essence" },
|
{ amount: 2e49, type: "essence" },
|
||||||
{ amount: 1.2e45, type: "crystals" },
|
{ amount: 1.2e45, type: "crystals" },
|
||||||
|
{ targetId: "astral_sovereign_1", type: "upgrade" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "reality_forge",
|
zoneId: "reality_forge",
|
||||||
@@ -1329,7 +1368,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 4e63, type: "gold" },
|
{ amount: 4e63, type: "gold" },
|
||||||
{ amount: 1.2e60, type: "essence" },
|
{ amount: 1.2e60, type: "essence" },
|
||||||
{ amount: 7e55, type: "crystals" },
|
{ amount: 7e55, type: "crystals" },
|
||||||
{ targetId: "astral_sovereign_1", type: "upgrade" },
|
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
@@ -1346,6 +1384,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 2e66, type: "gold" },
|
{ amount: 2e66, type: "gold" },
|
||||||
{ amount: 6e62, type: "essence" },
|
{ amount: 6e62, type: "essence" },
|
||||||
{ amount: 3.5e58, type: "crystals" },
|
{ amount: 3.5e58, type: "crystals" },
|
||||||
|
{ targetId: "reality_warden_1", type: "upgrade" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
@@ -1362,6 +1401,7 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 1e69, type: "gold" },
|
{ amount: 1e69, type: "gold" },
|
||||||
{ amount: 3e65, type: "essence" },
|
{ amount: 3e65, type: "essence" },
|
||||||
{ amount: 1.8e61, type: "crystals" },
|
{ amount: 1.8e61, type: "crystals" },
|
||||||
|
{ targetId: "infinity_ranger_1", type: "upgrade" },
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "cosmic_maelstrom",
|
zoneId: "cosmic_maelstrom",
|
||||||
@@ -1428,7 +1468,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 6e83, type: "gold" },
|
{ amount: 6e83, type: "gold" },
|
||||||
{ amount: 1.8e80, type: "essence" },
|
{ amount: 1.8e80, type: "essence" },
|
||||||
{ amount: 1e76, type: "crystals" },
|
{ amount: 1e76, type: "crystals" },
|
||||||
{ targetId: "primordial_mage_1", type: "upgrade" },
|
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "primeval_sanctum",
|
zoneId: "primeval_sanctum",
|
||||||
@@ -1510,7 +1549,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 2e108, type: "gold" },
|
{ amount: 2e108, type: "gold" },
|
||||||
{ amount: 6e104, type: "essence" },
|
{ amount: 6e104, type: "essence" },
|
||||||
{ amount: 3e100, type: "crystals" },
|
{ amount: 3e100, type: "crystals" },
|
||||||
{ targetId: "reality_warden_1", type: "upgrade" },
|
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "the_absolute",
|
zoneId: "the_absolute",
|
||||||
@@ -1543,7 +1581,6 @@ export const defaultQuests: Array<Quest> = [
|
|||||||
{ amount: 5e121, type: "gold" },
|
{ amount: 5e121, type: "gold" },
|
||||||
{ amount: 1.5e118, type: "essence" },
|
{ amount: 1.5e118, type: "essence" },
|
||||||
{ amount: 7e113, type: "crystals" },
|
{ amount: 7e113, type: "crystals" },
|
||||||
{ targetId: "infinity_ranger_1", type: "upgrade" },
|
|
||||||
],
|
],
|
||||||
status: "locked",
|
status: "locked",
|
||||||
zoneId: "the_absolute",
|
zoneId: "the_absolute",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
|||||||
zoneId: "verdant_vale",
|
zoneId: "verdant_vale",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { type: "combat_power", value: 1.08 },
|
bonus: { type: "combat_power", value: 1.12 },
|
||||||
description:
|
description:
|
||||||
"A ward fashioned from bark older than the kingdom. Its presence on the battlefield is not merely physical — something ancient watches through it.",
|
"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",
|
id: "elder_bark_shield",
|
||||||
@@ -75,7 +75,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
|||||||
zoneId: "frozen_peaks",
|
zoneId: "frozen_peaks",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { type: "gold_income", value: 1.1 },
|
bonus: { type: "gold_income", value: 1.15 },
|
||||||
description:
|
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.",
|
"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",
|
id: "void_fragment_amulet",
|
||||||
@@ -231,7 +231,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
|||||||
zoneId: "infernal_court",
|
zoneId: "infernal_court",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
bonus: { type: "essence_income", value: 1.15 },
|
bonus: { type: "essence_income", value: 1.2 },
|
||||||
description:
|
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.",
|
"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",
|
id: "soul_bound_catalyst",
|
||||||
@@ -492,6 +492,19 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
|||||||
],
|
],
|
||||||
zoneId: "abyssal_trench",
|
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 },
|
bonus: { type: "combat_power", value: 1.4 },
|
||||||
description:
|
description:
|
||||||
@@ -508,6 +521,18 @@ export const defaultRecipes: Array<CraftingRecipe> = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Zone 18: the_absolute
|
// 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 },
|
bonus: { type: "gold_income", value: 1.3 },
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
// ── Income multipliers ──────────────────────────────────────────────────────
|
// ── Income multipliers ──────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
category: "income",
|
category: "income",
|
||||||
cost: 5,
|
cost: 2,
|
||||||
description:
|
description:
|
||||||
"The echoes of past runs linger, amplifying your guild's income by 25%.",
|
"The echoes of past runs linger, amplifying your guild's income by 25%.",
|
||||||
id: "echo_income_1",
|
id: "echo_income_1",
|
||||||
@@ -20,7 +20,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "income",
|
category: "income",
|
||||||
cost: 10,
|
cost: 4,
|
||||||
description:
|
description:
|
||||||
"Your transcendent experience resonates through your guild, boosting income by 50%.",
|
"Your transcendent experience resonates through your guild, boosting income by 50%.",
|
||||||
id: "echo_income_2",
|
id: "echo_income_2",
|
||||||
@@ -29,7 +29,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "income",
|
category: "income",
|
||||||
cost: 20,
|
cost: 8,
|
||||||
description:
|
description:
|
||||||
"The harmony of multiple timelines surges through your guild, doubling its income.",
|
"The harmony of multiple timelines surges through your guild, doubling its income.",
|
||||||
id: "echo_income_3",
|
id: "echo_income_3",
|
||||||
@@ -38,7 +38,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "income",
|
category: "income",
|
||||||
cost: 40,
|
cost: 16,
|
||||||
description:
|
description:
|
||||||
"Ethereal energy overflows from your transcendence, tripling your guild's income.",
|
"Ethereal energy overflows from your transcendence, tripling your guild's income.",
|
||||||
id: "echo_income_4",
|
id: "echo_income_4",
|
||||||
@@ -47,7 +47,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "income",
|
category: "income",
|
||||||
cost: 80,
|
cost: 32,
|
||||||
description:
|
description:
|
||||||
"The infinite chorus of every run you've ever played amplifies your guild fivefold.",
|
"The infinite chorus of every run you've ever played amplifies your guild fivefold.",
|
||||||
id: "echo_income_5",
|
id: "echo_income_5",
|
||||||
@@ -58,7 +58,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
// ── Combat multipliers ──────────────────────────────────────────────────────
|
// ── Combat multipliers ──────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
category: "combat",
|
category: "combat",
|
||||||
cost: 5,
|
cost: 2,
|
||||||
description:
|
description:
|
||||||
"Memories of countless battles harden your adventurers, increasing party DPS by 25%.",
|
"Memories of countless battles harden your adventurers, increasing party DPS by 25%.",
|
||||||
id: "echo_combat_1",
|
id: "echo_combat_1",
|
||||||
@@ -67,7 +67,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "combat",
|
category: "combat",
|
||||||
cost: 15,
|
cost: 6,
|
||||||
description:
|
description:
|
||||||
"Veterans of transcendence know how to fight smarter, boosting party DPS by 50%.",
|
"Veterans of transcendence know how to fight smarter, boosting party DPS by 50%.",
|
||||||
id: "echo_combat_2",
|
id: "echo_combat_2",
|
||||||
@@ -76,7 +76,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "combat",
|
category: "combat",
|
||||||
cost: 35,
|
cost: 12,
|
||||||
description:
|
description:
|
||||||
"Your warriors carry the strength of every fallen timeline, doubling party DPS.",
|
"Your warriors carry the strength of every fallen timeline, doubling party DPS.",
|
||||||
id: "echo_combat_3",
|
id: "echo_combat_3",
|
||||||
@@ -87,7 +87,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
// ── Prestige threshold reductions ──────────────────────────────────────────
|
// ── Prestige threshold reductions ──────────────────────────────────────────
|
||||||
{
|
{
|
||||||
category: "prestige_threshold",
|
category: "prestige_threshold",
|
||||||
cost: 8,
|
cost: 3,
|
||||||
description:
|
description:
|
||||||
"Experience from past lives shortens the road to prestige — threshold reduced by 10%.",
|
"Experience from past lives shortens the road to prestige — threshold reduced by 10%.",
|
||||||
id: "echo_prestige_threshold_1",
|
id: "echo_prestige_threshold_1",
|
||||||
@@ -96,7 +96,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "prestige_threshold",
|
category: "prestige_threshold",
|
||||||
cost: 20,
|
cost: 6,
|
||||||
description:
|
description:
|
||||||
"You've walked this path so many times you know every shortcut — threshold reduced by 20%.",
|
"You've walked this path so many times you know every shortcut — threshold reduced by 20%.",
|
||||||
id: "echo_prestige_threshold_2",
|
id: "echo_prestige_threshold_2",
|
||||||
@@ -107,7 +107,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
// ── Prestige runestone multipliers ─────────────────────────────────────────
|
// ── Prestige runestone multipliers ─────────────────────────────────────────
|
||||||
{
|
{
|
||||||
category: "prestige_runestones",
|
category: "prestige_runestones",
|
||||||
cost: 8,
|
cost: 3,
|
||||||
description:
|
description:
|
||||||
"Transcendent insight attunes you to the runestones, earning 50% more per prestige.",
|
"Transcendent insight attunes you to the runestones, earning 50% more per prestige.",
|
||||||
id: "echo_prestige_runestones_1",
|
id: "echo_prestige_runestones_1",
|
||||||
@@ -116,7 +116,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "prestige_runestones",
|
category: "prestige_runestones",
|
||||||
cost: 20,
|
cost: 6,
|
||||||
description:
|
description:
|
||||||
"You have mastered the art of runestone crafting, doubling your prestige runestone yield.",
|
"You have mastered the art of runestone crafting, doubling your prestige runestone yield.",
|
||||||
id: "echo_prestige_runestones_2",
|
id: "echo_prestige_runestones_2",
|
||||||
@@ -127,7 +127,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
// ── Echo meta multipliers ───────────────────────────────────────────────────
|
// ── Echo meta multipliers ───────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
category: "echo_meta",
|
category: "echo_meta",
|
||||||
cost: 50,
|
cost: 25,
|
||||||
description:
|
description:
|
||||||
"Your transcendence resonates deeper, amplifying future echo yields by 25%.",
|
"Your transcendence resonates deeper, amplifying future echo yields by 25%.",
|
||||||
id: "echo_meta_1",
|
id: "echo_meta_1",
|
||||||
@@ -136,7 +136,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "echo_meta",
|
category: "echo_meta",
|
||||||
cost: 150,
|
cost: 75,
|
||||||
description:
|
description:
|
||||||
"Each loop of existence makes the next more powerful — future echo yields +50%.",
|
"Each loop of existence makes the next more powerful — future echo yields +50%.",
|
||||||
id: "echo_meta_2",
|
id: "echo_meta_2",
|
||||||
@@ -145,7 +145,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "echo_meta",
|
category: "echo_meta",
|
||||||
cost: 400,
|
cost: 200,
|
||||||
description:
|
description:
|
||||||
"You have mastered the infinite spiral of transcendence, doubling all future echo yields.",
|
"You have mastered the infinite spiral of transcendence, doubling all future echo yields.",
|
||||||
id: "echo_meta_3",
|
id: "echo_meta_3",
|
||||||
|
|||||||
@@ -642,6 +642,14 @@ const patchAdventurerStats = (state: GameState): number => {
|
|||||||
if (defaultAdventurer === undefined) {
|
if (defaultAdventurer === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const hasChanged
|
||||||
|
= savedAdventurer.baseCost !== defaultAdventurer.baseCost
|
||||||
|
|| savedAdventurer.class !== defaultAdventurer.class
|
||||||
|
|| savedAdventurer.combatPower !== defaultAdventurer.combatPower
|
||||||
|
|| savedAdventurer.essencePerSecond !== defaultAdventurer.essencePerSecond
|
||||||
|
|| savedAdventurer.goldPerSecond !== defaultAdventurer.goldPerSecond
|
||||||
|
|| savedAdventurer.level !== defaultAdventurer.level
|
||||||
|
|| savedAdventurer.name !== defaultAdventurer.name;
|
||||||
savedAdventurer.baseCost = defaultAdventurer.baseCost;
|
savedAdventurer.baseCost = defaultAdventurer.baseCost;
|
||||||
savedAdventurer.class = defaultAdventurer.class;
|
savedAdventurer.class = defaultAdventurer.class;
|
||||||
savedAdventurer.combatPower = defaultAdventurer.combatPower;
|
savedAdventurer.combatPower = defaultAdventurer.combatPower;
|
||||||
@@ -649,8 +657,10 @@ const patchAdventurerStats = (state: GameState): number => {
|
|||||||
savedAdventurer.goldPerSecond = defaultAdventurer.goldPerSecond;
|
savedAdventurer.goldPerSecond = defaultAdventurer.goldPerSecond;
|
||||||
savedAdventurer.level = defaultAdventurer.level;
|
savedAdventurer.level = defaultAdventurer.level;
|
||||||
savedAdventurer.name = defaultAdventurer.name;
|
savedAdventurer.name = defaultAdventurer.name;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -670,6 +680,15 @@ const patchQuestStats = (state: GameState): number => {
|
|||||||
if (defaultQuest === undefined) {
|
if (defaultQuest === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const savedPrereqs = JSON.stringify(savedQuest.prerequisiteIds);
|
||||||
|
const defaultPrereqs = JSON.stringify(defaultQuest.prerequisiteIds);
|
||||||
|
const hasChanged
|
||||||
|
= savedQuest.name !== defaultQuest.name
|
||||||
|
|| savedQuest.description !== defaultQuest.description
|
||||||
|
|| savedQuest.durationSeconds !== defaultQuest.durationSeconds
|
||||||
|
|| savedPrereqs !== defaultPrereqs
|
||||||
|
|| savedQuest.zoneId !== defaultQuest.zoneId
|
||||||
|
|| savedQuest.combatPowerRequired !== defaultQuest.combatPowerRequired;
|
||||||
savedQuest.name = defaultQuest.name;
|
savedQuest.name = defaultQuest.name;
|
||||||
savedQuest.description = defaultQuest.description;
|
savedQuest.description = defaultQuest.description;
|
||||||
savedQuest.durationSeconds = defaultQuest.durationSeconds;
|
savedQuest.durationSeconds = defaultQuest.durationSeconds;
|
||||||
@@ -678,8 +697,10 @@ const patchQuestStats = (state: GameState): number => {
|
|||||||
if (defaultQuest.combatPowerRequired !== undefined) {
|
if (defaultQuest.combatPowerRequired !== undefined) {
|
||||||
savedQuest.combatPowerRequired = defaultQuest.combatPowerRequired;
|
savedQuest.combatPowerRequired = defaultQuest.combatPowerRequired;
|
||||||
}
|
}
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -689,6 +710,7 @@ const patchQuestStats = (state: GameState): number => {
|
|||||||
* @param state - The player's current game state (mutated in place).
|
* @param state - The player's current game state (mutated in place).
|
||||||
* @returns The number of boss entries whose stats were updated.
|
* @returns The number of boss entries whose stats were updated.
|
||||||
*/
|
*/
|
||||||
|
/* eslint-disable-next-line complexity, max-statements -- Comparing many boss stat fields for change detection */
|
||||||
const patchBossStats = (state: GameState): number => {
|
const patchBossStats = (state: GameState): number => {
|
||||||
const defaultBossMap = new Map(defaultBosses.map((boss) => {
|
const defaultBossMap = new Map(defaultBosses.map((boss) => {
|
||||||
return [ boss.id, boss ] as const;
|
return [ boss.id, boss ] as const;
|
||||||
@@ -699,6 +721,20 @@ const patchBossStats = (state: GameState): number => {
|
|||||||
if (defaultBoss === undefined) {
|
if (defaultBoss === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const savedRewards = JSON.stringify(savedBoss.equipmentRewards);
|
||||||
|
const defaultRewards = JSON.stringify(defaultBoss.equipmentRewards);
|
||||||
|
const hasChanged
|
||||||
|
= savedBoss.name !== defaultBoss.name
|
||||||
|
|| savedBoss.description !== defaultBoss.description
|
||||||
|
|| savedBoss.maxHp !== defaultBoss.maxHp
|
||||||
|
|| savedBoss.damagePerSecond !== defaultBoss.damagePerSecond
|
||||||
|
|| savedBoss.goldReward !== defaultBoss.goldReward
|
||||||
|
|| savedBoss.essenceReward !== defaultBoss.essenceReward
|
||||||
|
|| savedBoss.crystalReward !== defaultBoss.crystalReward
|
||||||
|
|| savedRewards !== defaultRewards
|
||||||
|
|| savedBoss.prestigeRequirement !== defaultBoss.prestigeRequirement
|
||||||
|
|| savedBoss.zoneId !== defaultBoss.zoneId
|
||||||
|
|| savedBoss.bountyRunestones !== defaultBoss.bountyRunestones;
|
||||||
savedBoss.name = defaultBoss.name;
|
savedBoss.name = defaultBoss.name;
|
||||||
savedBoss.description = defaultBoss.description;
|
savedBoss.description = defaultBoss.description;
|
||||||
savedBoss.maxHp = defaultBoss.maxHp;
|
savedBoss.maxHp = defaultBoss.maxHp;
|
||||||
@@ -710,8 +746,10 @@ const patchBossStats = (state: GameState): number => {
|
|||||||
savedBoss.prestigeRequirement = defaultBoss.prestigeRequirement;
|
savedBoss.prestigeRequirement = defaultBoss.prestigeRequirement;
|
||||||
savedBoss.zoneId = defaultBoss.zoneId;
|
savedBoss.zoneId = defaultBoss.zoneId;
|
||||||
savedBoss.bountyRunestones = defaultBoss.bountyRunestones;
|
savedBoss.bountyRunestones = defaultBoss.bountyRunestones;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -731,13 +769,21 @@ const patchZoneStats = (state: GameState): number => {
|
|||||||
if (defaultZone === undefined) {
|
if (defaultZone === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const hasChanged
|
||||||
|
= savedZone.name !== defaultZone.name
|
||||||
|
|| savedZone.description !== defaultZone.description
|
||||||
|
|| savedZone.emoji !== defaultZone.emoji
|
||||||
|
|| savedZone.unlockBossId !== defaultZone.unlockBossId
|
||||||
|
|| savedZone.unlockQuestId !== defaultZone.unlockQuestId;
|
||||||
savedZone.name = defaultZone.name;
|
savedZone.name = defaultZone.name;
|
||||||
savedZone.description = defaultZone.description;
|
savedZone.description = defaultZone.description;
|
||||||
savedZone.emoji = defaultZone.emoji;
|
savedZone.emoji = defaultZone.emoji;
|
||||||
savedZone.unlockBossId = defaultZone.unlockBossId;
|
savedZone.unlockBossId = defaultZone.unlockBossId;
|
||||||
savedZone.unlockQuestId = defaultZone.unlockQuestId;
|
savedZone.unlockQuestId = defaultZone.unlockQuestId;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -747,6 +793,7 @@ const patchZoneStats = (state: GameState): number => {
|
|||||||
* @param state - The player's current game state (mutated in place).
|
* @param state - The player's current game state (mutated in place).
|
||||||
* @returns The number of upgrade entries whose stats were updated.
|
* @returns The number of upgrade entries whose stats were updated.
|
||||||
*/
|
*/
|
||||||
|
/* eslint-disable-next-line complexity -- Comparing many upgrade stat fields for change detection */
|
||||||
const patchUpgradeStats = (state: GameState): number => {
|
const patchUpgradeStats = (state: GameState): number => {
|
||||||
const defaultUpgradeMap = new Map(defaultUpgrades.map((upgrade) => {
|
const defaultUpgradeMap = new Map(defaultUpgrades.map((upgrade) => {
|
||||||
return [ upgrade.id, upgrade ] as const;
|
return [ upgrade.id, upgrade ] as const;
|
||||||
@@ -757,6 +804,15 @@ const patchUpgradeStats = (state: GameState): number => {
|
|||||||
if (defaultUpgrade === undefined) {
|
if (defaultUpgrade === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const hasChanged
|
||||||
|
= savedUpgrade.name !== defaultUpgrade.name
|
||||||
|
|| savedUpgrade.description !== defaultUpgrade.description
|
||||||
|
|| savedUpgrade.target !== defaultUpgrade.target
|
||||||
|
|| savedUpgrade.adventurerId !== defaultUpgrade.adventurerId
|
||||||
|
|| savedUpgrade.multiplier !== defaultUpgrade.multiplier
|
||||||
|
|| savedUpgrade.costGold !== defaultUpgrade.costGold
|
||||||
|
|| savedUpgrade.costEssence !== defaultUpgrade.costEssence
|
||||||
|
|| savedUpgrade.costCrystals !== defaultUpgrade.costCrystals;
|
||||||
savedUpgrade.name = defaultUpgrade.name;
|
savedUpgrade.name = defaultUpgrade.name;
|
||||||
savedUpgrade.description = defaultUpgrade.description;
|
savedUpgrade.description = defaultUpgrade.description;
|
||||||
savedUpgrade.target = defaultUpgrade.target;
|
savedUpgrade.target = defaultUpgrade.target;
|
||||||
@@ -767,8 +823,10 @@ const patchUpgradeStats = (state: GameState): number => {
|
|||||||
savedUpgrade.costGold = defaultUpgrade.costGold;
|
savedUpgrade.costGold = defaultUpgrade.costGold;
|
||||||
savedUpgrade.costEssence = defaultUpgrade.costEssence;
|
savedUpgrade.costEssence = defaultUpgrade.costEssence;
|
||||||
savedUpgrade.costCrystals = defaultUpgrade.costCrystals;
|
savedUpgrade.costCrystals = defaultUpgrade.costCrystals;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -778,6 +836,7 @@ const patchUpgradeStats = (state: GameState): number => {
|
|||||||
* @param state - The player's current game state (mutated in place).
|
* @param state - The player's current game state (mutated in place).
|
||||||
* @returns The number of equipment entries whose stats were updated.
|
* @returns The number of equipment entries whose stats were updated.
|
||||||
*/
|
*/
|
||||||
|
/* eslint-disable-next-line complexity, max-statements -- Comparing many equipment stat fields for change detection */
|
||||||
const patchEquipmentStats = (state: GameState): number => {
|
const patchEquipmentStats = (state: GameState): number => {
|
||||||
const defaultEquipmentMap = new Map(defaultEquipment.map((item) => {
|
const defaultEquipmentMap = new Map(defaultEquipment.map((item) => {
|
||||||
return [ item.id, item ] as const;
|
return [ item.id, item ] as const;
|
||||||
@@ -788,6 +847,18 @@ const patchEquipmentStats = (state: GameState): number => {
|
|||||||
if (defaultItem === undefined) {
|
if (defaultItem === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const savedBonus = JSON.stringify(savedItem.bonus);
|
||||||
|
const defaultBonus = JSON.stringify(defaultItem.bonus);
|
||||||
|
const savedCost = JSON.stringify(savedItem.cost);
|
||||||
|
const defaultCost = JSON.stringify(defaultItem.cost);
|
||||||
|
const hasChanged
|
||||||
|
= savedItem.name !== defaultItem.name
|
||||||
|
|| savedItem.description !== defaultItem.description
|
||||||
|
|| savedItem.type !== defaultItem.type
|
||||||
|
|| savedItem.rarity !== defaultItem.rarity
|
||||||
|
|| savedBonus !== defaultBonus
|
||||||
|
|| savedCost !== defaultCost
|
||||||
|
|| savedItem.setId !== defaultItem.setId;
|
||||||
savedItem.name = defaultItem.name;
|
savedItem.name = defaultItem.name;
|
||||||
savedItem.description = defaultItem.description;
|
savedItem.description = defaultItem.description;
|
||||||
savedItem.type = defaultItem.type;
|
savedItem.type = defaultItem.type;
|
||||||
@@ -799,8 +870,10 @@ const patchEquipmentStats = (state: GameState): number => {
|
|||||||
if (defaultItem.setId !== undefined) {
|
if (defaultItem.setId !== undefined) {
|
||||||
savedItem.setId = defaultItem.setId;
|
savedItem.setId = defaultItem.setId;
|
||||||
}
|
}
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -820,6 +893,16 @@ const patchAchievementStats = (state: GameState): number => {
|
|||||||
if (defaultAchievement === undefined) {
|
if (defaultAchievement === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const savedCondition = JSON.stringify(savedAchievement.condition);
|
||||||
|
const defaultCondition = JSON.stringify(defaultAchievement.condition);
|
||||||
|
const savedReward = JSON.stringify(savedAchievement.reward);
|
||||||
|
const defaultReward = JSON.stringify(defaultAchievement.reward);
|
||||||
|
const hasChanged
|
||||||
|
= savedAchievement.name !== defaultAchievement.name
|
||||||
|
|| savedAchievement.description !== defaultAchievement.description
|
||||||
|
|| savedAchievement.icon !== defaultAchievement.icon
|
||||||
|
|| savedCondition !== defaultCondition
|
||||||
|
|| savedReward !== defaultReward;
|
||||||
savedAchievement.name = defaultAchievement.name;
|
savedAchievement.name = defaultAchievement.name;
|
||||||
savedAchievement.description = defaultAchievement.description;
|
savedAchievement.description = defaultAchievement.description;
|
||||||
savedAchievement.icon = defaultAchievement.icon;
|
savedAchievement.icon = defaultAchievement.icon;
|
||||||
@@ -827,8 +910,10 @@ const patchAchievementStats = (state: GameState): number => {
|
|||||||
if (defaultAchievement.reward !== undefined) {
|
if (defaultAchievement.reward !== undefined) {
|
||||||
savedAchievement.reward = { ...defaultAchievement.reward };
|
savedAchievement.reward = { ...defaultAchievement.reward };
|
||||||
}
|
}
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,12 +102,23 @@ prestigeRouter.post("/", async(context) => {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
const now = Date.now();
|
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 */
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */
|
||||||
data: { state: finalState as object, updatedAt: now },
|
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({
|
await prisma.player.update({
|
||||||
data: {
|
data: {
|
||||||
characterName: state.player.characterName,
|
characterName: state.player.characterName,
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ const shuffleWithSeed = <T>(array: Array<T>, seed: number): Array<T> => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const challengeTypes: Array<DailyChallengeType> = [
|
const progressionChallengeTypes: Array<DailyChallengeType> = [
|
||||||
"clicks",
|
|
||||||
"bossesDefeated",
|
"bossesDefeated",
|
||||||
"questsCompleted",
|
"questsCompleted",
|
||||||
"prestige",
|
"prestige",
|
||||||
@@ -80,7 +79,8 @@ const challengeTypes: Array<DailyChallengeType> = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates 3 daily challenges for the given date string, deterministically.
|
* Generates 3 daily challenges for the given date string, deterministically.
|
||||||
* Picks one challenge from 3 different randomly-selected types.
|
* Always includes a "clicks" challenge (always completable regardless of
|
||||||
|
* progression), then picks 2 more from the remaining types.
|
||||||
* @param dateString - The date string (YYYY-MM-DD) to generate challenges for.
|
* @param dateString - The date string (YYYY-MM-DD) to generate challenges for.
|
||||||
* @returns An array of 3 DailyChallenge objects.
|
* @returns An array of 3 DailyChallenge objects.
|
||||||
*/
|
*/
|
||||||
@@ -88,8 +88,10 @@ const generateDailyChallenges = (
|
|||||||
dateString: string,
|
dateString: string,
|
||||||
): Array<DailyChallenge> => {
|
): Array<DailyChallenge> => {
|
||||||
const seed = dateSeed(dateString);
|
const seed = dateSeed(dateString);
|
||||||
const selectedTypes = shuffleWithSeed([ ...challengeTypes ], seed).
|
const selectedTypes: Array<DailyChallengeType> = [
|
||||||
slice(0, 3);
|
"clicks",
|
||||||
|
...shuffleWithSeed([ ...progressionChallengeTypes ], seed).slice(0, 2),
|
||||||
|
];
|
||||||
|
|
||||||
return selectedTypes.map((type, index) => {
|
return selectedTypes.map((type, index) => {
|
||||||
const templates = dailyChallengeTemplates.filter((template) => {
|
const templates = dailyChallengeTemplates.filter((template) => {
|
||||||
|
|||||||
@@ -15,14 +15,21 @@ import type {
|
|||||||
} from "@elysium/types";
|
} from "@elysium/types";
|
||||||
|
|
||||||
const basePrestigeGoldThreshold = 1_000_000;
|
const basePrestigeGoldThreshold = 1_000_000;
|
||||||
const thresholdScaleFactor = 5;
|
const runestonesPerPrestigeLevel = 15;
|
||||||
const runestonesPerPrestigeLevel = 10;
|
|
||||||
const milestoneInterval = 5;
|
const milestoneInterval = 5;
|
||||||
const milestoneRunestonesPerInterval = 25;
|
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.
|
* 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 prestigeCount - The current number of prestiges completed.
|
||||||
* @param thresholdMultiplier - An optional echo-upgrade multiplier applied to the threshold.
|
* @param thresholdMultiplier - An optional echo-upgrade multiplier applied to the threshold.
|
||||||
* @returns The gold amount required to prestige.
|
* @returns The gold amount required to prestige.
|
||||||
@@ -33,7 +40,7 @@ const calculatePrestigeThreshold = (
|
|||||||
): number => {
|
): number => {
|
||||||
return (
|
return (
|
||||||
basePrestigeGoldThreshold
|
basePrestigeGoldThreshold
|
||||||
* Math.pow(thresholdScaleFactor, prestigeCount)
|
* Math.pow(prestigeCount + 1, 2)
|
||||||
* thresholdMultiplier
|
* thresholdMultiplier
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -107,7 +114,9 @@ interface RunestoneParameters {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates how many runestones the player earns from a prestige.
|
* 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 - The parameters for the runestone calculation.
|
||||||
* @param parameters.totalGoldEarned - The total gold earned in the current run.
|
* @param parameters.totalGoldEarned - The total gold earned in the current run.
|
||||||
* @param parameters.prestigeCount - The current prestige count.
|
* @param parameters.prestigeCount - The current prestige count.
|
||||||
@@ -123,9 +132,11 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
|
|||||||
echoRunestoneMultiplier = 1,
|
echoRunestoneMultiplier = 1,
|
||||||
} = parameters;
|
} = parameters;
|
||||||
const threshold = calculatePrestigeThreshold(prestigeCount);
|
const threshold = calculatePrestigeThreshold(prestigeCount);
|
||||||
const base
|
const base = Math.min(
|
||||||
= Math.floor(Math.sqrt(totalGoldEarned / threshold))
|
Math.floor(Math.cbrt(totalGoldEarned / threshold))
|
||||||
* runestonesPerPrestigeLevel;
|
* runestonesPerPrestigeLevel,
|
||||||
|
maxBaseRunestones,
|
||||||
|
);
|
||||||
const runestoneMult = getCategoryMultiplier(
|
const runestoneMult = getCategoryMultiplier(
|
||||||
purchasedUpgradeIds,
|
purchasedUpgradeIds,
|
||||||
"runestones",
|
"runestones",
|
||||||
@@ -135,14 +146,15 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the new prestige production multiplier.
|
* 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.
|
* @param prestigeCount - The new prestige count.
|
||||||
* @returns The production multiplier for the new prestige level.
|
* @returns The production multiplier for the new prestige level.
|
||||||
*/
|
*/
|
||||||
const calculateProductionMultiplier = (
|
const calculateProductionMultiplier = (
|
||||||
prestigeCount: number,
|
prestigeCount: number,
|
||||||
): 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
|
* Preserve automation preferences across prestige — the player explicitly
|
||||||
* opted into these settings and would not expect them to silently reset.
|
* opted into these settings and would not expect them to silently reset.
|
||||||
*/
|
*/
|
||||||
|
autoAdventurer: currentState.autoAdventurer ?? false,
|
||||||
autoBoss: currentState.autoBoss ?? false,
|
autoBoss: currentState.autoBoss ?? false,
|
||||||
|
|
||||||
autoQuest: currentState.autoQuest ?? false,
|
autoQuest: currentState.autoQuest ?? false,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const finalBossId = "the_absolute_one";
|
|||||||
/**
|
/**
|
||||||
* Base constant used in the echo yield formula.
|
* Base constant used in the echo yield formula.
|
||||||
*/
|
*/
|
||||||
const echoFormulaConstant = 853;
|
const echoFormulaConstant = 224;
|
||||||
|
|
||||||
const getCategoryMultiplier = (
|
const getCategoryMultiplier = (
|
||||||
purchasedIds: Array<string>,
|
purchasedIds: Array<string>,
|
||||||
|
|||||||
@@ -595,6 +595,18 @@ describe("debug route", () => {
|
|||||||
expect(adventurer?.unlocked).toBe(true);
|
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 () => {
|
it("skips adventurer stat patching for adventurers not in defaults", async () => {
|
||||||
const state = makeState({
|
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"],
|
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");
|
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 () => {
|
it("skips quest stat patching for quests not in defaults", async () => {
|
||||||
const state = makeState({
|
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"],
|
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);
|
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 () => {
|
it("skips boss stat patching for bosses not in defaults", async () => {
|
||||||
const state = makeState({
|
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"],
|
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");
|
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 () => {
|
it("skips zone stat patching for zones not in defaults", async () => {
|
||||||
const state = makeState({
|
const state = makeState({
|
||||||
zones: [{ id: "nonexistent_zone_xyz", status: "unlocked", name: "Ghost", description: "Old", emoji: "❓", unlockBossId: null, unlockQuestId: null }] as GameState["zones"],
|
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);
|
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 () => {
|
it("skips upgrade stat patching for upgrades not in defaults", async () => {
|
||||||
const state = makeState({
|
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"],
|
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);
|
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 () => {
|
it("skips equipment stat patching for items not in defaults", async () => {
|
||||||
const state = makeState({
|
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"],
|
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();
|
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 () => {
|
it("skips achievement stat patching for achievements not in defaults", async () => {
|
||||||
const state = makeState({
|
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"],
|
achievements: [{ id: "nonexistent_achievement_xyz", unlockedAt: null, name: "Ghost", description: "Old", icon: "❓", condition: { type: "totalClicks", amount: 1 }, reward: undefined }] as GameState["achievements"],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { GameState } from "@elysium/types";
|
|||||||
vi.mock("../../src/db/client.js", () => ({
|
vi.mock("../../src/db/client.js", () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
player: { update: vi.fn() },
|
player: { update: vi.fn() },
|
||||||
gameState: { findUnique: vi.fn(), update: vi.fn() },
|
gameState: { findUnique: vi.fn(), update: vi.fn(), updateMany: vi.fn() },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ describe("prestige route", () => {
|
|||||||
let app: Hono;
|
let app: Hono;
|
||||||
let prisma: {
|
let prisma: {
|
||||||
player: { update: ReturnType<typeof vi.fn> };
|
player: { update: ReturnType<typeof vi.fn> };
|
||||||
gameState: { 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 () => {
|
beforeEach(async () => {
|
||||||
@@ -83,8 +83,8 @@ describe("prestige route", () => {
|
|||||||
|
|
||||||
it("returns runestones on successful prestige", async () => {
|
it("returns runestones on successful prestige", async () => {
|
||||||
const state = makeState();
|
const state = makeState();
|
||||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||||
const res = await post("");
|
const res = await post("");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
@@ -93,6 +93,14 @@ describe("prestige route", () => {
|
|||||||
expect(body.runestones).toBeGreaterThanOrEqual(0);
|
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 () => {
|
it("returns 500 when the database throws during prestige", async () => {
|
||||||
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
||||||
const res = await post("");
|
const res = await post("");
|
||||||
@@ -112,8 +120,8 @@ describe("prestige route", () => {
|
|||||||
challenges: [{ id: "prestige_challenge", type: "prestige", target: 2, progress: 0, completed: false, crystalReward: 5 }],
|
challenges: [{ id: "prestige_challenge", type: "prestige", target: 2, progress: 0, completed: false, crystalReward: 5 }],
|
||||||
} as GameState["dailyChallenges"],
|
} as GameState["dailyChallenges"],
|
||||||
});
|
});
|
||||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||||
const res = await post("");
|
const res = await post("");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ describe("transcendence route", () => {
|
|||||||
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
const res = await post("/buy-upgrade", { upgradeId: "echo_income_1" });
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
const body = await res.json() as { echoesRemaining: number; purchasedUpgradeIds: string[] };
|
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");
|
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));
|
expect(a.map((c) => c.id)).toEqual(b.map((c) => c.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("always includes a clicks challenge regardless of date", async () => {
|
||||||
|
vi.setSystemTime(LA_MIDNIGHT_2024_01_15);
|
||||||
|
const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js");
|
||||||
|
const day1 = generateDailyChallenges("2024-01-15");
|
||||||
|
const day2 = generateDailyChallenges("2024-01-16");
|
||||||
|
expect(day1.some((c) => c.type === "clicks")).toBe(true);
|
||||||
|
expect(day2.some((c) => c.type === "clicks")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("generates different challenges for different dates", async () => {
|
it("generates different challenges for different dates", async () => {
|
||||||
vi.setSystemTime(LA_MIDNIGHT_2024_01_15);
|
vi.setSystemTime(LA_MIDNIGHT_2024_01_15);
|
||||||
const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js");
|
const { generateDailyChallenges } = await import("../../src/services/dailyChallenges.js");
|
||||||
const day1 = generateDailyChallenges("2024-01-15");
|
const day1 = generateDailyChallenges("2024-01-15");
|
||||||
const day2 = generateDailyChallenges("2024-01-16");
|
const day2 = generateDailyChallenges("2024-01-16");
|
||||||
// They should differ in at least one challenge ID (types vary by seed)
|
// The 2 non-clicks types should vary by seed between dates
|
||||||
expect(day1.map((c) => c.type)).not.toEqual(day2.map((c) => c.type));
|
const day1NonClicks = day1.filter((c) => c.type !== "clicks").map((c) => c.type);
|
||||||
|
const day2NonClicks = day2.filter((c) => c.type !== "clicks").map((c) => c.type);
|
||||||
|
expect(day1NonClicks).not.toEqual(day2NonClicks);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,15 +55,18 @@ const makeMinimalState = (overrides: Partial<GameState> = {}): GameState =>
|
|||||||
|
|
||||||
describe("calculatePrestigeThreshold", () => {
|
describe("calculatePrestigeThreshold", () => {
|
||||||
it("returns base threshold at count 0", () => {
|
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);
|
expect(calculatePrestigeThreshold(0)).toBe(1_000_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 5× at count 1", () => {
|
it("returns 4× base at count 1", () => {
|
||||||
expect(calculatePrestigeThreshold(1)).toBe(5_000_000);
|
// base × (1+1)^2 = 1_000_000 × 4 = 4_000_000
|
||||||
|
expect(calculatePrestigeThreshold(1)).toBe(4_000_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 25× at count 2", () => {
|
it("returns 9× base at count 2", () => {
|
||||||
expect(calculatePrestigeThreshold(2)).toBe(25_000_000);
|
// base × (2+1)^2 = 1_000_000 × 9 = 9_000_000
|
||||||
|
expect(calculatePrestigeThreshold(2)).toBe(9_000_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies threshold multiplier correctly", () => {
|
it("applies threshold multiplier correctly", () => {
|
||||||
@@ -99,21 +102,27 @@ describe("isEligibleForPrestige", () => {
|
|||||||
|
|
||||||
describe("calculateRunestones", () => {
|
describe("calculateRunestones", () => {
|
||||||
it("calculates basic runestones formula", () => {
|
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: [] });
|
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
|
||||||
expect(result).toBe(20);
|
expect(result).toBe(15);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies echo runestone multiplier", () => {
|
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 });
|
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", () => {
|
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"] });
|
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);
|
expect(calculateProductionMultiplier(0)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 1.15 at count 1", () => {
|
it("returns 1.25 at count 1", () => {
|
||||||
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.15);
|
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scales exponentially", () => {
|
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", () => {
|
describe("calculateEchoes", () => {
|
||||||
it("handles prestige count of 0 by treating it as 1", () => {
|
it("handles prestige count of 0 by treating it as 1", () => {
|
||||||
// safeCount = max(0, 1) = 1; floor(853 / sqrt(1)) = 853
|
// safeCount = max(0, 1) = 1; floor(224 / sqrt(1)) = 224
|
||||||
expect(calculateEchoes(0, 1)).toBe(853);
|
expect(calculateEchoes(0, 1)).toBe(224);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calculates echoes at count 1", () => {
|
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", () => {
|
it("decreases echoes with higher prestige count", () => {
|
||||||
const echoesAt1 = calculateEchoes(1, 1);
|
const echoesAt1 = calculateEchoes(1, 1);
|
||||||
const echoesAt4 = calculateEchoes(4, 1);
|
const echoesAt4 = calculateEchoes(4, 1);
|
||||||
expect(echoesAt4).toBeLessThan(echoesAt1);
|
expect(echoesAt4).toBeLessThan(echoesAt1);
|
||||||
// floor(853 / sqrt(4)) = floor(853 / 2) = 426
|
// floor(224 / sqrt(4)) = floor(224 / 2) = 112
|
||||||
expect(echoesAt4).toBe(426);
|
expect(echoesAt4).toBe(112);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies echoMetaMultiplier", () => {
|
it("applies echoMetaMultiplier", () => {
|
||||||
@@ -118,6 +119,11 @@ describe("calculateEchoes", () => {
|
|||||||
const withMult = calculateEchoes(1, 2);
|
const withMult = calculateEchoes(1, 2);
|
||||||
expect(withMult).toBe(base * 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", () => {
|
describe("buildPostTranscendenceState", () => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
/* eslint-disable complexity -- Complex component with many render paths */
|
/* eslint-disable complexity -- Complex component with many render paths */
|
||||||
import { type JSX, useState } from "react";
|
import { type JSX, useState } from "react";
|
||||||
import { useGame } from "../../context/gameContext.js";
|
import { useGame } from "../../context/gameContext.js";
|
||||||
|
import { computeEffectiveAdventurerStats } from "../../engine/tick.js";
|
||||||
import { cdnImage } from "../../utils/cdn.js";
|
import { cdnImage } from "../../utils/cdn.js";
|
||||||
import { LockToggle } from "../ui/lockToggle.js";
|
import { LockToggle } from "../ui/lockToggle.js";
|
||||||
import type { Adventurer } from "@elysium/types";
|
import type { Adventurer } from "@elysium/types";
|
||||||
@@ -76,12 +77,19 @@ const computeMaxAffordable = (adventurer: Adventurer, gold: number): number => {
|
|||||||
return quantity;
|
return quantity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface EffectiveAdventurerStats {
|
||||||
|
readonly combatPower: number;
|
||||||
|
readonly essencePerSecond: number;
|
||||||
|
readonly goldPerSecond: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface AdventurerCardProperties {
|
interface AdventurerCardProperties {
|
||||||
readonly adventurer: Adventurer;
|
readonly adventurer: Adventurer;
|
||||||
readonly currentGold: number;
|
readonly currentGold: number;
|
||||||
readonly batchSize: BatchSize;
|
readonly batchSize: BatchSize;
|
||||||
readonly unlockHint: string | undefined;
|
readonly unlockHint: string | undefined;
|
||||||
readonly formatNumber: (n: number)=> string;
|
readonly formatNumber: (n: number)=> string;
|
||||||
|
readonly effectiveStats: EffectiveAdventurerStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +100,7 @@ interface AdventurerCardProperties {
|
|||||||
* @param props.batchSize - The selected batch size.
|
* @param props.batchSize - The selected batch size.
|
||||||
* @param props.unlockHint - Optional quest name that unlocks this adventurer.
|
* @param props.unlockHint - Optional quest name that unlocks this adventurer.
|
||||||
* @param props.formatNumber - The number formatting utility function.
|
* @param props.formatNumber - The number formatting utility function.
|
||||||
|
* @param props.effectiveStats - The post-multiplier per-unit stats.
|
||||||
* @returns The JSX element.
|
* @returns The JSX element.
|
||||||
*/
|
*/
|
||||||
const AdventurerCard = ({
|
const AdventurerCard = ({
|
||||||
@@ -100,6 +109,7 @@ const AdventurerCard = ({
|
|||||||
batchSize,
|
batchSize,
|
||||||
unlockHint,
|
unlockHint,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
effectiveStats,
|
||||||
}: AdventurerCardProperties): JSX.Element => {
|
}: AdventurerCardProperties): JSX.Element => {
|
||||||
const { buyAdventurer } = useGame();
|
const { buyAdventurer } = useGame();
|
||||||
|
|
||||||
@@ -134,17 +144,17 @@ const AdventurerCard = ({
|
|||||||
<div className="adventurer-info">
|
<div className="adventurer-info">
|
||||||
<h3>{adventurer.name}</h3>
|
<h3>{adventurer.name}</h3>
|
||||||
<p>
|
<p>
|
||||||
{formatNumber(adventurer.goldPerSecond)}
|
{formatNumber(effectiveStats.goldPerSecond)}
|
||||||
{" gold/s each"}
|
{" gold/s each"}
|
||||||
</p>
|
</p>
|
||||||
{adventurer.essencePerSecond > 0
|
{adventurer.essencePerSecond > 0
|
||||||
&& <p>
|
&& <p>
|
||||||
{formatNumber(adventurer.essencePerSecond)}
|
{formatNumber(effectiveStats.essencePerSecond)}
|
||||||
{" essence/s each"}
|
{" essence/s each"}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<p>
|
<p>
|
||||||
{formatNumber(adventurer.combatPower)}
|
{formatNumber(effectiveStats.combatPower)}
|
||||||
{" combat power each"}
|
{" combat power each"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,6 +290,10 @@ const AdventurerPanel = (): JSX.Element => {
|
|||||||
adventurer={adventurer}
|
adventurer={adventurer}
|
||||||
batchSize={batchSize}
|
batchSize={batchSize}
|
||||||
currentGold={state.resources.gold}
|
currentGold={state.resources.gold}
|
||||||
|
effectiveStats={computeEffectiveAdventurerStats(
|
||||||
|
state,
|
||||||
|
adventurer.id,
|
||||||
|
)}
|
||||||
formatNumber={formatNumber}
|
formatNumber={formatNumber}
|
||||||
key={adventurer.id}
|
key={adventurer.id}
|
||||||
unlockHint={adventurerUnlockHints.get(adventurer.id)}
|
unlockHint={adventurerUnlockHints.get(adventurer.id)}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
/* eslint-disable max-lines -- Boss panel with sub-component and helper function */
|
/* eslint-disable max-lines -- Boss panel with sub-component and helper function */
|
||||||
import { type JSX, useState } from "react";
|
import { type JSX, useState } from "react";
|
||||||
import { useGame } from "../../context/gameContext.js";
|
import { useGame } from "../../context/gameContext.js";
|
||||||
|
import { computePartyCombatPower } from "../../engine/tick.js";
|
||||||
import { cdnImage } from "../../utils/cdn.js";
|
import { cdnImage } from "../../utils/cdn.js";
|
||||||
import { LockToggle } from "../ui/lockToggle.js";
|
import { LockToggle } from "../ui/lockToggle.js";
|
||||||
import { ZoneSelector } from "./zoneSelector.js";
|
import { ZoneSelector } from "./zoneSelector.js";
|
||||||
import type { Boss, GameState } from "@elysium/types";
|
import type { Boss } from "@elysium/types";
|
||||||
|
|
||||||
interface BossCardProperties {
|
interface BossCardProperties {
|
||||||
readonly boss: Boss;
|
readonly boss: Boss;
|
||||||
@@ -157,72 +158,6 @@ const BossCard = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes party DPS and HP from the current game state.
|
|
||||||
* @param state - The full game state.
|
|
||||||
* @returns The computed party DPS and HP values.
|
|
||||||
*/
|
|
||||||
const computePartyStats = (
|
|
||||||
state: GameState,
|
|
||||||
): {
|
|
||||||
partyDps: number;
|
|
||||||
partyHp: number;
|
|
||||||
} => {
|
|
||||||
const { upgrades, adventurers, equipment, prestige } = state;
|
|
||||||
let globalMultiplier = 1;
|
|
||||||
for (const upgrade of upgrades) {
|
|
||||||
const { purchased, target, multiplier } = upgrade;
|
|
||||||
if (purchased && target === "global") {
|
|
||||||
globalMultiplier = globalMultiplier * multiplier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const prestigeBonus = prestige.count * 0.1;
|
|
||||||
const prestigeMultiplier = 1 + prestigeBonus;
|
|
||||||
const equipmentCombatMultiplier = equipment.
|
|
||||||
filter((item) => {
|
|
||||||
return item.equipped && item.bonus.combatMultiplier !== undefined;
|
|
||||||
}).
|
|
||||||
reduce((multiplier, item) => {
|
|
||||||
return multiplier * (item.bonus.combatMultiplier ?? 1);
|
|
||||||
}, 1);
|
|
||||||
|
|
||||||
let partyDps = 0;
|
|
||||||
let partyHp = 0;
|
|
||||||
for (const adventurer of adventurers) {
|
|
||||||
const { count, id: adventurerId, combatPower, level } = adventurer;
|
|
||||||
if (count === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let adventurerMultiplier = 1;
|
|
||||||
for (const upgrade of upgrades) {
|
|
||||||
const {
|
|
||||||
purchased,
|
|
||||||
target,
|
|
||||||
multiplier,
|
|
||||||
adventurerId: upgradeAdventurerId,
|
|
||||||
} = upgrade;
|
|
||||||
if (
|
|
||||||
purchased
|
|
||||||
&& target === "adventurer"
|
|
||||||
&& upgradeAdventurerId === adventurerId
|
|
||||||
) {
|
|
||||||
adventurerMultiplier = adventurerMultiplier * multiplier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dps
|
|
||||||
= combatPower
|
|
||||||
* count
|
|
||||||
* adventurerMultiplier
|
|
||||||
* globalMultiplier
|
|
||||||
* prestigeMultiplier;
|
|
||||||
partyDps = partyDps + dps;
|
|
||||||
const hp = level * 50 * count;
|
|
||||||
partyHp = partyHp + hp;
|
|
||||||
}
|
|
||||||
partyDps = partyDps * equipmentCombatMultiplier;
|
|
||||||
return { partyDps, partyHp };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the boss panel with zone selection and boss list.
|
* Renders the boss panel with zone selection and boss list.
|
||||||
* @returns The JSX element.
|
* @returns The JSX element.
|
||||||
@@ -266,7 +201,14 @@ const BossPanel = (): JSX.Element => {
|
|||||||
void handleChallenge(bossId);
|
void handleChallenge(bossId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { zones, bosses, quests, autoBoss, prestige: playerPrestige } = state;
|
const {
|
||||||
|
adventurers,
|
||||||
|
autoBoss,
|
||||||
|
bosses,
|
||||||
|
prestige: playerPrestige,
|
||||||
|
quests,
|
||||||
|
zones,
|
||||||
|
} = state;
|
||||||
|
|
||||||
const activeZone = zones.find((zone) => {
|
const activeZone = zones.find((zone) => {
|
||||||
return zone.id === activeZoneId;
|
return zone.id === activeZoneId;
|
||||||
@@ -349,7 +291,12 @@ const BossPanel = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const autoBossOn = autoBoss === true;
|
const autoBossOn = autoBoss === true;
|
||||||
const { partyDps, partyHp } = computePartyStats(state);
|
const partyDps = computePartyCombatPower(state);
|
||||||
|
let partyHp = 0;
|
||||||
|
for (const { level, count } of adventurers) {
|
||||||
|
// eslint-disable-next-line stylistic/no-mixed-operators -- level * 50 * count is clear
|
||||||
|
partyHp = partyHp + level * 50 * count;
|
||||||
|
}
|
||||||
const { count: prestigeCount } = playerPrestige;
|
const { count: prestigeCount } = playerPrestige;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const categoryOrder: Array<PrestigeUpgradeCategory> = [
|
|||||||
const PrestigePanel = (): JSX.Element => {
|
const PrestigePanel = (): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
reload,
|
reloadSilent,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
buyPrestigeUpgrade,
|
buyPrestigeUpgrade,
|
||||||
enableNotifications,
|
enableNotifications,
|
||||||
@@ -141,7 +141,7 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
`You've reached prestige level ${data.newPrestigeCount.toString()}!`,
|
`You've reached prestige level ${data.newPrestigeCount.toString()}!`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await reload();
|
await reloadSilent();
|
||||||
} catch (error_: unknown) {
|
} catch (error_: unknown) {
|
||||||
setPrestigeError(
|
setPrestigeError(
|
||||||
error_ instanceof Error
|
error_ instanceof Error
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
/* eslint-disable max-statements -- Many local variables needed for quest state */
|
/* eslint-disable max-statements -- Many local variables needed for quest state */
|
||||||
import { useState, type JSX } from "react";
|
import { useState, type JSX } from "react";
|
||||||
import { useGame } from "../../context/gameContext.js";
|
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 { cdnImage } from "../../utils/cdn.js";
|
||||||
import { LockToggle } from "../ui/lockToggle.js";
|
import { LockToggle } from "../ui/lockToggle.js";
|
||||||
import { ZoneSelector } from "./zoneSelector.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) => {
|
const activeZone = zones.find((zone) => {
|
||||||
return zone.id === activeZoneId;
|
return zone.id === activeZoneId;
|
||||||
@@ -226,11 +229,7 @@ const QuestPanel = (): JSX.Element => {
|
|||||||
: quests.find((quest) => {
|
: quests.find((quest) => {
|
||||||
return quest.id === activeZone.unlockQuestId;
|
return quest.id === activeZone.unlockQuestId;
|
||||||
});
|
});
|
||||||
let partyCombatPower = 0;
|
const partyCombatPower = computePartyCombatPower(state);
|
||||||
for (const adventurer of adventurers) {
|
|
||||||
const contribution = adventurer.combatPower * adventurer.count;
|
|
||||||
partyCombatPower = partyCombatPower + contribution;
|
|
||||||
}
|
|
||||||
const zoneQuests = quests.filter(({ zoneId }) => {
|
const zoneQuests = quests.filter(({ zoneId }) => {
|
||||||
return zoneId === activeZoneId;
|
return zoneId === activeZoneId;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
|
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
|
||||||
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
|
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
|
||||||
/* eslint-disable complexity -- UpgradeCard has many conditional render paths for states */
|
/* eslint-disable complexity -- UpgradeCard has many conditional render paths for states */
|
||||||
|
/* eslint-disable max-statements -- UpgradePanel builds hints from three sources */
|
||||||
|
/* eslint-disable max-lines -- Upgrade panel with sub-component exceeds line limit */
|
||||||
import { type JSX, useState } from "react";
|
import { type JSX, useState } from "react";
|
||||||
import { useGame } from "../../context/gameContext.js";
|
import { useGame } from "../../context/gameContext.js";
|
||||||
import { cdnImage } from "../../utils/cdn.js";
|
import { cdnImage } from "../../utils/cdn.js";
|
||||||
@@ -238,6 +240,22 @@ const UpgradePanel = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const upgrade of locked) {
|
||||||
|
if (
|
||||||
|
!upgradeUnlockHints.has(upgrade.id)
|
||||||
|
&& upgrade.adventurerId !== undefined
|
||||||
|
) {
|
||||||
|
const adventurerForHint = adventurers.find((a) => {
|
||||||
|
return a.id === upgrade.adventurerId;
|
||||||
|
});
|
||||||
|
if (adventurerForHint !== undefined) {
|
||||||
|
upgradeUnlockHints.set(
|
||||||
|
upgrade.id,
|
||||||
|
`🗡️ Recruit: ${adventurerForHint.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleToggle(): void {
|
function handleToggle(): void {
|
||||||
setShowLocked((current) => {
|
setShowLocked((current) => {
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
/* eslint-disable complexity -- Many conditional resource and badge render paths */
|
/* eslint-disable complexity -- Many conditional resource and badge render paths */
|
||||||
import { useState, type FocusEvent, type JSX } from "react";
|
import { useState, type FocusEvent, type JSX } from "react";
|
||||||
import { useGame } from "../../context/gameContext.js";
|
import { useGame } from "../../context/gameContext.js";
|
||||||
import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js";
|
import {
|
||||||
|
RESOURCE_CAP,
|
||||||
|
computeEssencePerSecond,
|
||||||
|
computeGoldPerSecond,
|
||||||
|
computePartyCombatPower,
|
||||||
|
} from "../../engine/tick.js";
|
||||||
import type { Resource } from "@elysium/types";
|
import type { Resource } from "@elysium/types";
|
||||||
|
|
||||||
interface ResourceBarProperties {
|
interface ResourceBarProperties {
|
||||||
@@ -83,12 +88,11 @@ const ResourceBar = ({
|
|||||||
const { gold, essence, crystals } = resources;
|
const { gold, essence, crystals } = resources;
|
||||||
let partyCombatPower = 0;
|
let partyCombatPower = 0;
|
||||||
let goldPerSecond = 0;
|
let goldPerSecond = 0;
|
||||||
|
let essencePerSecond = 0;
|
||||||
if (state !== null) {
|
if (state !== null) {
|
||||||
for (const adventurer of state.adventurers) {
|
partyCombatPower = computePartyCombatPower(state);
|
||||||
const contribution = adventurer.combatPower * adventurer.count;
|
|
||||||
partyCombatPower = partyCombatPower + contribution;
|
|
||||||
}
|
|
||||||
goldPerSecond = computeGoldPerSecond(state);
|
goldPerSecond = computeGoldPerSecond(state);
|
||||||
|
essencePerSecond = computeEssencePerSecond(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarUrl: string | null = null;
|
let avatarUrl: string | null = null;
|
||||||
@@ -182,6 +186,13 @@ const ResourceBar = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className="resource-label">{"Gold/s"}</span>
|
<span className="resource-label">{"Gold/s"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="resource">
|
||||||
|
<span className="resource-icon">{"⚡"}</span>
|
||||||
|
<span className="resource-value">
|
||||||
|
{formatNumber(essencePerSecond)}
|
||||||
|
</span>
|
||||||
|
<span className="resource-label">{"Essence/s"}</span>
|
||||||
|
</div>
|
||||||
<div className={`resource${essenceFull
|
<div className={`resource${essenceFull
|
||||||
? " resource-full"
|
? " resource-full"
|
||||||
: ""}`}>
|
: ""}`}>
|
||||||
|
|||||||
@@ -53,11 +53,13 @@ import {
|
|||||||
transcend as transcendApi,
|
transcend as transcendApi,
|
||||||
} from "../api/client.js";
|
} from "../api/client.js";
|
||||||
import { CODEX_ENTRIES } from "../data/codex.js";
|
import { CODEX_ENTRIES } from "../data/codex.js";
|
||||||
|
import { EXPLORATION_AREAS } from "../data/explorations.js";
|
||||||
import { RECIPES } from "../data/recipes.js";
|
import { RECIPES } from "../data/recipes.js";
|
||||||
import {
|
import {
|
||||||
RESOURCE_CAP,
|
RESOURCE_CAP,
|
||||||
applyTick,
|
applyTick,
|
||||||
calculateClickPower,
|
calculateClickPower,
|
||||||
|
computePartyCombatPower,
|
||||||
} from "../engine/tick.js";
|
} from "../engine/tick.js";
|
||||||
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
||||||
import { formatNumber as formatNumberUtil } from "../utils/format.js";
|
import { formatNumber as formatNumberUtil } from "../utils/format.js";
|
||||||
@@ -115,6 +117,9 @@ const applyBossResult = (
|
|||||||
}).
|
}).
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
);
|
);
|
||||||
|
const newlyUnlockedZoneIds = new Set(unlockedZones.map((z) => {
|
||||||
|
return z.id;
|
||||||
|
}));
|
||||||
|
|
||||||
const challengeUpdate
|
const challengeUpdate
|
||||||
= previous.dailyChallenges === undefined
|
= previous.dailyChallenges === undefined
|
||||||
@@ -215,6 +220,23 @@ const applyBossResult = (
|
|||||||
? { ...u, unlocked: true }
|
? { ...u, unlocked: true }
|
||||||
: u;
|
: 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;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +310,12 @@ interface GameContextValue {
|
|||||||
*/
|
*/
|
||||||
reload: ()=> Promise<void>;
|
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).
|
* Unix timestamp of the last successful cloud save (null until first save response).
|
||||||
*/
|
*/
|
||||||
@@ -696,6 +724,10 @@ export const GameProvider = ({
|
|||||||
|
|
||||||
/* No-op placeholder */
|
/* No-op placeholder */
|
||||||
});
|
});
|
||||||
|
const reloadSilentReference = useRef<()=> Promise<void>>(async() => {
|
||||||
|
|
||||||
|
/* No-op placeholder */
|
||||||
|
});
|
||||||
const [ schemaOutdated, setSchemaOutdated ] = useState(false);
|
const [ schemaOutdated, setSchemaOutdated ] = useState(false);
|
||||||
const [ saveSchemaVersion, setSaveSchemaVersion ] = useState(0);
|
const [ saveSchemaVersion, setSaveSchemaVersion ] = useState(0);
|
||||||
const [ currentSchemaVersion, setCurrentSchemaVersion ] = useState(0);
|
const [ currentSchemaVersion, setCurrentSchemaVersion ] = useState(0);
|
||||||
@@ -783,6 +815,32 @@ export const GameProvider = ({
|
|||||||
|
|
||||||
reloadReference.current = reload;
|
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(() => {
|
useEffect(() => {
|
||||||
enableSoundsReference.current = enableSounds;
|
enableSoundsReference.current = enableSounds;
|
||||||
}, [ enableSounds ]);
|
}, [ enableSounds ]);
|
||||||
@@ -1078,11 +1136,7 @@ export const GameProvider = ({
|
|||||||
return q.status === "active";
|
return q.status === "active";
|
||||||
});
|
});
|
||||||
if (!hasActiveQuest) {
|
if (!hasActiveQuest) {
|
||||||
// eslint-disable-next-line unicorn/no-array-reduce -- Need the total!
|
const partyCombatPower = computePartyCombatPower(next);
|
||||||
const partyCombatPower = next.adventurers.reduce((total, a) => {
|
|
||||||
const power = total + a.combatPower;
|
|
||||||
return power * a.count;
|
|
||||||
}, 0);
|
|
||||||
const zoneOrder = new Map(
|
const zoneOrder = new Map(
|
||||||
next.zones.map((z, index) => {
|
next.zones.map((z, index) => {
|
||||||
return [ z.id, index ];
|
return [ z.id, index ];
|
||||||
@@ -1120,14 +1174,31 @@ export const GameProvider = ({
|
|||||||
next.autoAdventurer === true
|
next.autoAdventurer === true
|
||||||
&& next.prestige.purchasedUpgradeIds.includes("auto_adventurer")
|
&& next.prestige.purchasedUpgradeIds.includes("auto_adventurer")
|
||||||
) {
|
) {
|
||||||
|
const maxAdventurerLevel = Math.max(
|
||||||
|
...next.adventurers.
|
||||||
|
filter((a) => {
|
||||||
|
return a.unlocked;
|
||||||
|
}).
|
||||||
|
map((a) => {
|
||||||
|
return a.level;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const autoBuyCap = 100;
|
||||||
const [ bestAdventurer ] = next.adventurers.
|
const [ bestAdventurer ] = next.adventurers.
|
||||||
filter((adventurer) => {
|
filter((adventurer) => {
|
||||||
const cost
|
const cost
|
||||||
= adventurer.baseCost * Math.pow(1.15, adventurer.count);
|
= adventurer.baseCost * Math.pow(1.15, adventurer.count);
|
||||||
return adventurer.unlocked && next.resources.gold >= cost;
|
const isMaxTier = adventurer.level === maxAdventurerLevel;
|
||||||
|
const withinCap
|
||||||
|
= isMaxTier || adventurer.count < autoBuyCap;
|
||||||
|
return (
|
||||||
|
adventurer.unlocked
|
||||||
|
&& next.resources.gold >= cost
|
||||||
|
&& withinCap
|
||||||
|
);
|
||||||
}).
|
}).
|
||||||
sort((adventurerA, adventurerB) => {
|
sort((adventurerA, adventurerB) => {
|
||||||
return adventurerB.combatPower - adventurerA.combatPower;
|
return adventurerB.level - adventurerA.level;
|
||||||
});
|
});
|
||||||
if (bestAdventurer !== undefined) {
|
if (bestAdventurer !== undefined) {
|
||||||
const purchaseCost
|
const purchaseCost
|
||||||
@@ -1280,7 +1351,7 @@ export const GameProvider = ({
|
|||||||
if (enableNotificationsReference.current) {
|
if (enableNotificationsReference.current) {
|
||||||
sendNotification("⭐ Prestige!", "You have ascended!");
|
sendNotification("⭐ Prestige!", "You have ascended!");
|
||||||
}
|
}
|
||||||
await reloadReference.current();
|
await reloadSilentReference.current();
|
||||||
}).
|
}).
|
||||||
catch(() => {
|
catch(() => {
|
||||||
|
|
||||||
@@ -1346,6 +1417,13 @@ export const GameProvider = ({
|
|||||||
}
|
}
|
||||||
return afterBoss;
|
return afterBoss;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Boss fight modifies server state; clear stale signature so
|
||||||
|
* the next pre-save or auto-save does not send a mismatched one.
|
||||||
|
*/
|
||||||
|
signatureReference.current = null;
|
||||||
|
localStorage.removeItem("elysium_save_signature");
|
||||||
setAutoBossLastResult({
|
setAutoBossLastResult({
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
bossName: bossName,
|
bossName: bossName,
|
||||||
@@ -1789,7 +1867,18 @@ export const GameProvider = ({
|
|||||||
|
|
||||||
const collectExploration = useCallback(
|
const collectExploration = useCallback(
|
||||||
async(areaId: string): Promise<ExploreCollectResponse> => {
|
async(areaId: string): Promise<ExploreCollectResponse> => {
|
||||||
|
isSyncingReference.current = true;
|
||||||
const result = await collectExplorationApi({ areaId });
|
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) => {
|
setState((previous) => {
|
||||||
if (previous?.exploration === undefined) {
|
if (previous?.exploration === undefined) {
|
||||||
return previous;
|
return previous;
|
||||||
@@ -2320,6 +2409,7 @@ export const GameProvider = ({
|
|||||||
offlineEssence,
|
offlineEssence,
|
||||||
offlineGold,
|
offlineGold,
|
||||||
reload,
|
reload,
|
||||||
|
reloadSilent,
|
||||||
resetProgress,
|
resetProgress,
|
||||||
saveSchemaVersion,
|
saveSchemaVersion,
|
||||||
schemaOutdated,
|
schemaOutdated,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
getActiveCompanionBonus,
|
getActiveCompanionBonus,
|
||||||
} from "@elysium/types";
|
} from "@elysium/types";
|
||||||
import { EQUIPMENT_SETS } from "../data/equipmentSets.js";
|
import { EQUIPMENT_SETS } from "../data/equipmentSets.js";
|
||||||
|
import { EXPLORATION_AREAS } from "../data/explorations.js";
|
||||||
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,6 +196,257 @@ export const computeGoldPerSecond = (state: GameState): number => {
|
|||||||
return goldPerSecond;
|
return goldPerSecond;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the current essence per second for the given game state,
|
||||||
|
* applying all relevant multipliers (upgrades, prestige, echo, crafted, companion).
|
||||||
|
* @param state - The current game state.
|
||||||
|
* @returns The total essence per second.
|
||||||
|
*/
|
||||||
|
export const computeEssencePerSecond = (state: GameState): number => {
|
||||||
|
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
||||||
|
const craftedEssenceMultiplier
|
||||||
|
= state.exploration?.craftedEssenceMultiplier ?? 1;
|
||||||
|
const companionBonus = getActiveCompanionBonus(
|
||||||
|
state.companions?.activeCompanionId,
|
||||||
|
state.companions?.unlockedCompanionIds ?? [],
|
||||||
|
);
|
||||||
|
const companionEssenceMult
|
||||||
|
= companionBonus?.type === "essenceIncome"
|
||||||
|
? 1 + companionBonus.value
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
let essencePerSecond = 0;
|
||||||
|
for (const adventurer of state.adventurers) {
|
||||||
|
if (!adventurer.unlocked || adventurer.count === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const upgradeMultiplier = state.upgrades.
|
||||||
|
filter((upgrade) => {
|
||||||
|
const isGlobal = upgrade.target === "global";
|
||||||
|
const isThisAdventurer
|
||||||
|
= upgrade.target === "adventurer"
|
||||||
|
&& upgrade.adventurerId === adventurer.id;
|
||||||
|
return upgrade.purchased && (isGlobal || isThisAdventurer);
|
||||||
|
}).
|
||||||
|
reduce((mult, upgrade) => {
|
||||||
|
return mult * upgrade.multiplier;
|
||||||
|
}, 1);
|
||||||
|
const contribution
|
||||||
|
= adventurer.essencePerSecond
|
||||||
|
* adventurer.count
|
||||||
|
* upgradeMultiplier
|
||||||
|
* state.prestige.productionMultiplier
|
||||||
|
* runestonesEssence
|
||||||
|
* craftedEssenceMultiplier
|
||||||
|
* companionEssenceMult;
|
||||||
|
essencePerSecond = essencePerSecond + contribution;
|
||||||
|
}
|
||||||
|
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 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.
|
||||||
|
*/
|
||||||
|
export const computePartyCombatPower = (state: GameState): number => {
|
||||||
|
let globalMultiplier = 1;
|
||||||
|
for (const upgrade of state.upgrades) {
|
||||||
|
if (upgrade.purchased && upgrade.target === "global") {
|
||||||
|
globalMultiplier = globalMultiplier * upgrade.multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear
|
||||||
|
const prestigeMultiplier = 1 + state.prestige.count * 0.1;
|
||||||
|
|
||||||
|
const equipmentCombatMultiplier = state.equipment.
|
||||||
|
filter((item) => {
|
||||||
|
return item.equipped && item.bonus.combatMultiplier !== undefined;
|
||||||
|
}).
|
||||||
|
reduce((mult, item) => {
|
||||||
|
return mult * (item.bonus.combatMultiplier ?? 1);
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
const equippedItemIds = state.equipment.
|
||||||
|
filter((item) => {
|
||||||
|
return item.equipped;
|
||||||
|
}).
|
||||||
|
map((item) => {
|
||||||
|
return item.id;
|
||||||
|
});
|
||||||
|
const { combatMultiplier: setCombatMultiplier } = computeSetBonuses(
|
||||||
|
equippedItemIds,
|
||||||
|
EQUIPMENT_SETS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const echoCombatMultiplier
|
||||||
|
= state.transcendence?.echoCombatMultiplier ?? 1;
|
||||||
|
const craftedCombatMultiplier
|
||||||
|
= state.exploration?.craftedCombatMultiplier ?? 1;
|
||||||
|
|
||||||
|
const companionBonus = getActiveCompanionBonus(
|
||||||
|
state.companions?.activeCompanionId,
|
||||||
|
state.companions?.unlockedCompanionIds ?? [],
|
||||||
|
);
|
||||||
|
const companionCombatMult
|
||||||
|
= companionBonus?.type === "bossDamage"
|
||||||
|
? 1 + companionBonus.value
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
let partyCombatPower = 0;
|
||||||
|
for (const adventurer of state.adventurers) {
|
||||||
|
if (adventurer.count === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let adventurerMultiplier = 1;
|
||||||
|
for (const upgrade of state.upgrades) {
|
||||||
|
if (
|
||||||
|
upgrade.purchased
|
||||||
|
&& upgrade.target === "adventurer"
|
||||||
|
&& upgrade.adventurerId === adventurer.id
|
||||||
|
) {
|
||||||
|
adventurerMultiplier = adventurerMultiplier * upgrade.multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const contribution
|
||||||
|
= adventurer.combatPower
|
||||||
|
* adventurer.count
|
||||||
|
* adventurerMultiplier
|
||||||
|
* globalMultiplier
|
||||||
|
* prestigeMultiplier;
|
||||||
|
partyCombatPower = partyCombatPower + contribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
return partyCombatPower
|
||||||
|
* equipmentCombatMultiplier
|
||||||
|
* setCombatMultiplier
|
||||||
|
* echoCombatMultiplier
|
||||||
|
* craftedCombatMultiplier
|
||||||
|
* companionCombatMult;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pure function — applies one game tick to the state.
|
* Pure function — applies one game tick to the state.
|
||||||
* DeltaSeconds: time elapsed since last tick.
|
* DeltaSeconds: time elapsed since last tick.
|
||||||
@@ -469,6 +721,19 @@ export const applyTick = (
|
|||||||
challengeCrystals = result.crystalsAwarded;
|
challengeCrystals = result.crystalsAwarded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-unlock adventurer-specific upgrades when their adventurer is recruited
|
||||||
|
updatedUpgrades = updatedUpgrades.map((upgrade) => {
|
||||||
|
if (upgrade.unlocked || upgrade.adventurerId === undefined) {
|
||||||
|
return upgrade;
|
||||||
|
}
|
||||||
|
const adventurer = updatedAdventurers.find((a) => {
|
||||||
|
return a.id === upgrade.adventurerId;
|
||||||
|
});
|
||||||
|
return adventurer !== undefined && adventurer.count > 0
|
||||||
|
? { ...upgrade, unlocked: true }
|
||||||
|
: upgrade;
|
||||||
|
});
|
||||||
|
|
||||||
const goldValue = capResource(state.resources.gold + goldGained + questGold);
|
const goldValue = capResource(state.resources.gold + goldGained + questGold);
|
||||||
const essenceValue = capResource(
|
const essenceValue = capResource(
|
||||||
state.resources.essence + essenceGained + questEssence,
|
state.resources.essence + essenceGained + questEssence,
|
||||||
@@ -489,6 +754,23 @@ export const applyTick = (
|
|||||||
...updatedDailyChallenges === undefined
|
...updatedDailyChallenges === undefined
|
||||||
? {}
|
? {}
|
||||||
: { dailyChallenges: updatedDailyChallenges },
|
: { 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,
|
adventurers: updatedAdventurers,
|
||||||
bosses: updatedBosses,
|
bosses: updatedBosses,
|
||||||
equipment: updatedEquipmentReference,
|
equipment: updatedEquipmentReference,
|
||||||
|
|||||||
Reference in New Issue
Block a user