2 Commits

Author SHA1 Message Date
hikari 7b81f6cb33 fix: resolve auto-boss signature mismatch, expose full CP, cap auto-buy, show unlock hints
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m6s
CI / Lint, Build & Test (pull_request) Failing after 1m9s
Closes #148: clear stale signature after each boss fight so subsequent
auto-boss pre-saves don't send a mismatched HMAC.

Closes #151: auto-buy skips non-max-tier adventurers once they reach 100,
keeping gold flowing to the highest-unlocked tier.

Closes #152: introduce computePartyCombatPower() in tick.ts mirroring the
server-side formula (global upgrades, prestige, equipment, set bonuses,
echo, crafted, companion). Resource bar, auto-quest gate, and boss panel
all now use the same multiplier-accurate value.

Closes #146: tick engine auto-unlocks adventurer-specific upgrades when
their adventurer is first recruited; upgrade panel shows a recruit hint
for locked entries with no boss/quest source.
2026-03-25 16:38:42 -07:00
hikari ad4fcc2811 fix: resolve sync count inflation, add essence/s display, sort auto-buy by level
Closes #147: patch functions now detect actual changes before incrementing
the patched counter, preventing inflated sync reports.

Closes #149: computeEssencePerSecond exported from tick engine and shown
in the resource bar dropdown alongside Gold/s.

Closes #150: auto-buy now sorts adventurers by level descending for
semantic clarity, ensuring highest-tier units are purchased first.
2026-03-25 16:16:21 -07:00
14 changed files with 436 additions and 271 deletions
+58 -58
View File
@@ -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: [ "dark_templar_1" ], upgradeRewards: [],
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: 1, prestigeRequirement: 6,
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: 2, prestigeRequirement: 7,
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: 2, prestigeRequirement: 8,
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: 2, prestigeRequirement: 9,
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: 2, prestigeRequirement: 10,
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: 2, prestigeRequirement: 9,
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: 2, prestigeRequirement: 10,
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: 2, prestigeRequirement: 11,
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: 3, prestigeRequirement: 12,
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: 3, prestigeRequirement: 13,
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: 3, prestigeRequirement: 12,
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: 3, prestigeRequirement: 13,
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: 3, prestigeRequirement: 14,
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: 3, prestigeRequirement: 15,
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: 4, prestigeRequirement: 16,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "infernal_court", zoneId: "infernal_court",
@@ -626,7 +626,7 @@ export const defaultBosses: Array<Boss> = [
id: "prism_golem", id: "prism_golem",
maxHp: 2e16, maxHp: 2e16,
name: "The Prism Golem", name: "The Prism Golem",
prestigeRequirement: 3, prestigeRequirement: 15,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
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: 4, prestigeRequirement: 16,
status: "locked", status: "locked",
upgradeRewards: [ "void_ascendancy" ], upgradeRewards: [ "void_ascendancy" ],
zoneId: "crystalline_spire", zoneId: "crystalline_spire",
@@ -662,7 +662,7 @@ export const defaultBosses: Array<Boss> = [
id: "the_faceted", id: "the_faceted",
maxHp: 3e17, maxHp: 3e17,
name: "The Faceted", name: "The Faceted",
prestigeRequirement: 4, prestigeRequirement: 17,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "crystalline_spire", zoneId: "crystalline_spire",
@@ -680,7 +680,7 @@ export const defaultBosses: Array<Boss> = [
id: "diamond_colossus", id: "diamond_colossus",
maxHp: 1e18, maxHp: 1e18,
name: "The Diamond Colossus", name: "The Diamond Colossus",
prestigeRequirement: 4, prestigeRequirement: 18,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "crystalline_spire", zoneId: "crystalline_spire",
@@ -698,7 +698,7 @@ export const defaultBosses: Array<Boss> = [
id: "crystal_sovereign", id: "crystal_sovereign",
maxHp: 4e18, maxHp: 4e18,
name: "The Crystal Sovereign", name: "The Crystal Sovereign",
prestigeRequirement: 4, prestigeRequirement: 19,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "crystalline_spire", zoneId: "crystalline_spire",
@@ -717,7 +717,7 @@ export const defaultBosses: Array<Boss> = [
id: "void_herald", id: "void_herald",
maxHp: 1e19, maxHp: 1e19,
name: "The Void Herald", name: "The Void Herald",
prestigeRequirement: 4, prestigeRequirement: 18,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
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: 4, prestigeRequirement: 19,
status: "locked", status: "locked",
upgradeRewards: [ "divine_harmony" ], upgradeRewards: [ "divine_harmony" ],
zoneId: "void_sanctum", zoneId: "void_sanctum",
@@ -753,7 +753,7 @@ export const defaultBosses: Array<Boss> = [
id: "the_unmaker", id: "the_unmaker",
maxHp: 2e20, maxHp: 2e20,
name: "The Unmaker", name: "The Unmaker",
prestigeRequirement: 5, prestigeRequirement: 20,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
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: 5, prestigeRequirement: 21,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "void_sanctum", zoneId: "void_sanctum",
@@ -789,7 +789,7 @@ export const defaultBosses: Array<Boss> = [
id: "void_emperor", id: "void_emperor",
maxHp: 3e21, maxHp: 3e21,
name: "The Void Emperor", name: "The Void Emperor",
prestigeRequirement: 5, prestigeRequirement: 22,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "void_sanctum", zoneId: "void_sanctum",
@@ -808,7 +808,7 @@ export const defaultBosses: Array<Boss> = [
id: "throne_warden", id: "throne_warden",
maxHp: 1e22, maxHp: 1e22,
name: "The Throne Warden", name: "The Throne Warden",
prestigeRequirement: 5, prestigeRequirement: 21,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
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: 5, prestigeRequirement: 22,
status: "locked", status: "locked",
upgradeRewards: [ "infernal_fury" ], upgradeRewards: [ "infernal_fury" ],
zoneId: "eternal_throne", zoneId: "eternal_throne",
@@ -844,7 +844,7 @@ export const defaultBosses: Array<Boss> = [
id: "the_undying", id: "the_undying",
maxHp: 2e23, maxHp: 2e23,
name: "The Undying", name: "The Undying",
prestigeRequirement: 5, prestigeRequirement: 23,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
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: 5, prestigeRequirement: 24,
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: 6, prestigeRequirement: 25,
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: 6, prestigeRequirement: 26,
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: 6, prestigeRequirement: 27,
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: 7, prestigeRequirement: 29,
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: 7, prestigeRequirement: 31,
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: 8, prestigeRequirement: 33,
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: 8, prestigeRequirement: 35,
status: "locked", status: "locked",
upgradeRewards: [ "oblivion_paladin_1" ], upgradeRewards: [],
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: 8, prestigeRequirement: 37,
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: 9, prestigeRequirement: 39,
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: 9, prestigeRequirement: 41,
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: 10, prestigeRequirement: 44,
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: 11, prestigeRequirement: 47,
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: 11, prestigeRequirement: 49,
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: 12, prestigeRequirement: 51,
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: 12, prestigeRequirement: 54,
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: 13, prestigeRequirement: 57,
status: "locked", status: "locked",
upgradeRewards: [ "transcendent_rogue_1" ], upgradeRewards: [],
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: 13, prestigeRequirement: 59,
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: 14, prestigeRequirement: 61,
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: 15, prestigeRequirement: 65,
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: 16, prestigeRequirement: 69,
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: 17, prestigeRequirement: 74,
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: 17, prestigeRequirement: 76,
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: 18, prestigeRequirement: 79,
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: 19, prestigeRequirement: 83,
status: "locked", status: "locked",
upgradeRewards: [ "omniversal_champion_1" ], upgradeRewards: [],
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: 20, prestigeRequirement: 88,
status: "locked", status: "locked",
upgradeRewards: [], upgradeRewards: [],
zoneId: "the_absolute", zoneId: "the_absolute",
+16 -51
View File
@@ -157,21 +157,6 @@ export const defaultQuests: Array<Quest> = [
status: "locked", status: "locked",
zoneId: "frozen_peaks", 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" },
],
status: "locked",
zoneId: "frozen_peaks",
},
{ {
combatPowerRequired: 400_000, combatPowerRequired: 400_000,
description: description:
@@ -179,7 +164,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: [ "glacier_tomb" ], prerequisiteIds: [ "frozen_wastes" ],
rewards: [ rewards: [
{ amount: 5000, type: "essence" }, { amount: 5000, type: "essence" },
{ amount: 200, type: "crystals" }, { amount: 200, type: "crystals" },
@@ -203,22 +188,6 @@ 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: 5_000_000,
@@ -229,8 +198,7 @@ export const defaultQuests: Array<Quest> = [
name: "The Shadow Mere", name: "The Shadow Mere",
prerequisiteIds: [], prerequisiteIds: [],
rewards: [ rewards: [
{ amount: 5_000_000, type: "gold" }, { amount: 150, type: "essence" },
{ amount: 5000, type: "essence" },
], ],
status: "locked", status: "locked",
zoneId: "shadow_marshes", zoneId: "shadow_marshes",
@@ -244,8 +212,7 @@ export const defaultQuests: Array<Quest> = [
name: "The Witch Coven", name: "The Witch Coven",
prerequisiteIds: [ "shadow_mere" ], prerequisiteIds: [ "shadow_mere" ],
rewards: [ rewards: [
{ amount: 20_000_000, type: "gold" }, { amount: 500, type: "essence" },
{ amount: 20_000, type: "essence" },
{ targetId: "shadow_assassin", type: "adventurer" }, { targetId: "shadow_assassin", type: "adventurer" },
], ],
status: "locked", status: "locked",
@@ -278,9 +245,9 @@ export const defaultQuests: Array<Quest> = [
name: "The Plague Ruins", name: "The Plague Ruins",
prerequisiteIds: [ "sunken_temple" ], prerequisiteIds: [ "sunken_temple" ],
rewards: [ rewards: [
{ amount: 100_000_000, type: "gold" }, { amount: 8_000_000, type: "gold" },
{ amount: 30_000, type: "essence" }, { amount: 2000, type: "essence" },
{ amount: 500, type: "crystals" }, { amount: 150, type: "crystals" },
{ targetId: "dark_templar", type: "adventurer" }, { targetId: "dark_templar", type: "adventurer" },
], ],
status: "locked", status: "locked",
@@ -362,9 +329,8 @@ export const defaultQuests: Array<Quest> = [
name: "Void Rift", name: "Void Rift",
prerequisiteIds: [], prerequisiteIds: [],
rewards: [ rewards: [
{ amount: 2_000_000_000, type: "gold" }, { amount: 500, type: "crystals" },
{ amount: 300_000, type: "essence" }, { amount: 5000, type: "essence" },
{ amount: 1000, type: "crystals" },
], ],
status: "locked", status: "locked",
zoneId: "astral_void", zoneId: "astral_void",
@@ -378,9 +344,9 @@ export const defaultQuests: Array<Quest> = [
name: "The Star Graveyard", name: "The Star Graveyard",
prerequisiteIds: [ "void_rift" ], prerequisiteIds: [ "void_rift" ],
rewards: [ rewards: [
{ amount: 8_000_000_000, type: "gold" }, { amount: 1_000_000_000, type: "gold" },
{ amount: 800_000, type: "essence" }, { amount: 100_000, type: "essence" },
{ amount: 3000, type: "crystals" }, { amount: 1000, type: "crystals" },
], ],
status: "locked", status: "locked",
zoneId: "astral_void", zoneId: "astral_void",
@@ -394,9 +360,8 @@ export const defaultQuests: Array<Quest> = [
name: "Between Worlds", name: "Between Worlds",
prerequisiteIds: [ "star_graveyard" ], prerequisiteIds: [ "star_graveyard" ],
rewards: [ rewards: [
{ amount: 25_000_000_000, type: "gold" }, { amount: 250_000, type: "essence" },
{ amount: 2_000_000, type: "essence" }, { amount: 2000, type: "crystals" },
{ amount: 8000, type: "crystals" },
{ targetId: "divine_champion", type: "adventurer" }, { targetId: "divine_champion", type: "adventurer" },
], ],
status: "locked", status: "locked",
@@ -411,9 +376,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: 80_000_000_000, type: "gold" }, { amount: 10_000_000_000, type: "gold" },
{ amount: 5_000_000, type: "essence" }, { amount: 1_000_000, type: "essence" },
{ amount: 20_000, type: "crystals" }, { amount: 10_000, type: "crystals" },
], ],
status: "locked", status: "locked",
zoneId: "astral_void", zoneId: "astral_void",
+15 -15
View File
@@ -11,7 +11,7 @@ export const defaultTranscendenceUpgrades: Array<TranscendenceUpgrade> = [
// ── Income multipliers ────────────────────────────────────────────────────── // ── Income multipliers ──────────────────────────────────────────────────────
{ {
category: "income", category: "income",
cost: 2, cost: 5,
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: 4, cost: 10,
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: 8, cost: 20,
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: 16, cost: 40,
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: 32, cost: 80,
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: 2, cost: 5,
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: 6, cost: 15,
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: 12, cost: 35,
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: 3, cost: 8,
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: 6, cost: 20,
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: 3, cost: 8,
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: 6, cost: 20,
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: 25, cost: 50,
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: 75, cost: 150,
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: 200, cost: 400,
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",
+85
View File
@@ -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;
}; };
+9 -21
View File
@@ -15,21 +15,14 @@ import type {
} from "@elysium/types"; } from "@elysium/types";
const basePrestigeGoldThreshold = 1_000_000; const basePrestigeGoldThreshold = 1_000_000;
const thresholdScaleFactor = 5;
const runestonesPerPrestigeLevel = 10; 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 * (count + 1)^2 — polynomial growth that peaks around prestige 810 * Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
* 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.
@@ -40,7 +33,7 @@ const calculatePrestigeThreshold = (
): number => { ): number => {
return ( return (
basePrestigeGoldThreshold basePrestigeGoldThreshold
* Math.pow(prestigeCount + 1, 2) * Math.pow(thresholdScaleFactor, prestigeCount)
* thresholdMultiplier * thresholdMultiplier
); );
}; };
@@ -114,9 +107,7 @@ interface RunestoneParameters {
/** /**
* Calculates how many runestones the player earns from a prestige. * Calculates how many runestones the player earns from a prestige.
* Formula: min(floor(cbrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL, MAX_BASE) * multipliers. * Formula: floor(sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL * runestoneMultiplier.
* 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.
@@ -132,11 +123,9 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
echoRunestoneMultiplier = 1, echoRunestoneMultiplier = 1,
} = parameters; } = parameters;
const threshold = calculatePrestigeThreshold(prestigeCount); const threshold = calculatePrestigeThreshold(prestigeCount);
const base = Math.min( const base
Math.floor(Math.cbrt(totalGoldEarned / threshold)) = Math.floor(Math.sqrt(totalGoldEarned / threshold))
* runestonesPerPrestigeLevel, * runestonesPerPrestigeLevel;
maxBaseRunestones,
);
const runestoneMult = getCategoryMultiplier( const runestoneMult = getCategoryMultiplier(
purchasedUpgradeIds, purchasedUpgradeIds,
"runestones", "runestones",
@@ -146,15 +135,14 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
/** /**
* Calculates the new prestige production multiplier. * Calculates the new prestige production multiplier.
* Formula: 1.25^prestigeCount — exponential scaling per prestige that eventually * Formula: 1.15^prestigeCount — exponential scaling per prestige.
* 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.25, prestigeCount); return Math.pow(1.15, prestigeCount);
}; };
/** /**
+1 -1
View File
@@ -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 = 224; const echoFormulaConstant = 853;
const getCategoryMultiplier = ( const getCategoryMultiplier = (
purchasedIds: Array<string>, purchasedIds: Array<string>,
+1 -1
View File
@@ -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(98); // 100 - 2 expect(body.echoesRemaining).toBe(95); // 100 - 5
expect(body.purchasedUpgradeIds).toContain("echo_income_1"); expect(body.purchasedUpgradeIds).toContain("echo_income_1");
}); });
+16 -25
View File
@@ -55,18 +55,15 @@ 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 4× base at count 1", () => { it("returns 5× at count 1", () => {
// base × (1+1)^2 = 1_000_000 × 4 = 4_000_000 expect(calculatePrestigeThreshold(1)).toBe(5_000_000);
expect(calculatePrestigeThreshold(1)).toBe(4_000_000);
}); });
it("returns 9× base at count 2", () => { it("returns 25× at count 2", () => {
// base × (2+1)^2 = 1_000_000 × 9 = 9_000_000 expect(calculatePrestigeThreshold(2)).toBe(25_000_000);
expect(calculatePrestigeThreshold(2)).toBe(9_000_000);
}); });
it("applies threshold multiplier correctly", () => { it("applies threshold multiplier correctly", () => {
@@ -102,27 +99,21 @@ describe("isEligibleForPrestige", () => {
describe("calculateRunestones", () => { describe("calculateRunestones", () => {
it("calculates basic runestones formula", () => { it("calculates basic runestones formula", () => {
// floor(cbrt(4_000_000 / 1_000_000)) × 10 = floor(cbrt(4)) × 10 = 1 × 10 = 10 // floor(sqrt(4_000_000 / 1_000_000)) × 10 = floor(2) × 10 = 20
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
expect(result).toBe(10);
});
it("applies echo runestone multiplier", () => {
// floor(cbrt(4)) × 10 = 10; × 2 = 20
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
expect(result).toBe(20); expect(result).toBe(20);
}); });
it("applies purchased runestone upgrade multiplier", () => { it("applies echo runestone multiplier", () => {
// With "runestone_gain_1" purchased (multiplier 1.25): floor(10 × 1.25) = 12 // floor(sqrt(4) × 10) = 20; × 2 = 40
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
expect(result).toBe(12); expect(result).toBe(40);
}); });
it("caps base runestones before multipliers", () => { it("applies purchased runestone upgrade multiplier", () => {
// cbrt(9_261_000_000 / 1_000_000) = cbrt(9261) = 21 → 21 × 10 = 210, capped at 200 // With "runestones_1" purchased (multiplier 1.25): floor(20 × 1.25) = 25
const result = calculateRunestones({ totalGoldEarned: 9_261_000_000, prestigeCount: 0, purchasedUpgradeIds: [] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] });
expect(result).toBe(200); expect(result).toBeGreaterThan(20);
}); });
}); });
@@ -131,12 +122,12 @@ describe("calculateProductionMultiplier", () => {
expect(calculateProductionMultiplier(0)).toBe(1); expect(calculateProductionMultiplier(0)).toBe(1);
}); });
it("returns 1.25 at count 1", () => { it("returns 1.15 at count 1", () => {
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.25); expect(calculateProductionMultiplier(1)).toBeCloseTo(1.15);
}); });
it("scales exponentially", () => { it("scales exponentially", () => {
expect(calculateProductionMultiplier(10)).toBeCloseTo(Math.pow(1.25, 10)); expect(calculateProductionMultiplier(10)).toBeCloseTo(Math.pow(1.15, 10));
}); });
}); });
+5 -11
View File
@@ -97,21 +97,20 @@ 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(224 / sqrt(1)) = 224 // safeCount = max(0, 1) = 1; floor(853 / sqrt(1)) = 853
expect(calculateEchoes(0, 1)).toBe(224); expect(calculateEchoes(0, 1)).toBe(853);
}); });
it("calculates echoes at count 1", () => { it("calculates echoes at count 1", () => {
// floor(224 / sqrt(1)) = 224 expect(calculateEchoes(1, 1)).toBe(853);
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(224 / sqrt(4)) = floor(224 / 2) = 112 // floor(853 / sqrt(4)) = floor(853 / 2) = 426
expect(echoesAt4).toBe(112); expect(echoesAt4).toBe(426);
}); });
it("applies echoMetaMultiplier", () => { it("applies echoMetaMultiplier", () => {
@@ -119,11 +118,6 @@ 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", () => {
+16 -69
View File
@@ -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 (
@@ -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) => {
+16 -5
View File
@@ -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"
: ""}`}> : ""}`}>
+28 -7
View File
@@ -58,6 +58,7 @@ 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";
@@ -1078,11 +1079,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 +1117,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
@@ -1346,6 +1360,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,
+145
View File
@@ -195,6 +195,138 @@ 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 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.
* @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 +601,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,