diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f8b28e4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# Elysium Project Notes + +## About Page + +The About page (`apps/web/src/components/game/AboutPanel.tsx`) contains a **How to Play** guide that should be kept up to date as new features are added to the game. When implementing new game systems, zones, mechanics, or significant UI features, update the `HOW_TO_PLAY` array in `AboutPanel.tsx` to include a description of the new feature. diff --git a/IDEAS.md b/IDEAS.md index 34a3e8d..daadede 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -20,7 +20,7 @@ A running list of planned features and content additions. Strike through items a ## 📦 Content Additions -- [ ] **Equipment set bonuses** — Group existing equipment into named sets (e.g. "Shadow Infiltrator"). Wearing 2/3/4 pieces of a set grants escalating bonuses. Adds strategic depth without requiring lots of new items. +- [x] **Equipment set bonuses** — Group existing equipment into named sets (e.g. "Shadow Infiltrator"). Wearing 2/3/4 pieces of a set grants escalating bonuses. Adds strategic depth without requiring lots of new items. - [ ] **The Codex / Lore Book** — Defeating bosses and completing quests unlocks lore entries about the world. Pure flavour, but gives the world depth and a collection mechanic. Show a ✨ notification when new lore unlocks. @@ -43,7 +43,7 @@ A running list of planned features and content additions. Strike through items a 3. ~~Daily challenges~~ ✅ 4. ~~Boss first-kill bounties~~ ✅ 5. ~~Milestone prestige bonuses~~ ✅ -6. Equipment set bonuses (medium effort) +6. ~~Equipment set bonuses~~ ✅ 7. Auto-prestige toggle (prestige shop upgrade) 8. The Codex / Lore Book (flavour, lower priority) 9. Second prestige layer / Transcendence (big feature, save for later) diff --git a/apps/api/src/data/bosses.ts b/apps/api/src/data/bosses.ts index 2f19cec..8de18f4 100644 --- a/apps/api/src/data/bosses.ts +++ b/apps/api/src/data/bosses.ts @@ -18,6 +18,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, zoneId: "verdant_vale", + bountyRunestones: 1, }, { id: "lich_queen", @@ -35,6 +36,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["enchanted_blade", "plate_armour", "arcane_orb"], prestigeRequirement: 0, zoneId: "verdant_vale", + bountyRunestones: 2, }, { id: "forest_giant", @@ -52,6 +54,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["hide_armour"], prestigeRequirement: 0, zoneId: "verdant_vale", + bountyRunestones: 3, }, // ── Shattered Ruins ─────────────────────────────────────────────────────── { @@ -70,6 +73,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 0, zoneId: "shattered_ruins", + bountyRunestones: 3, }, { id: "bone_colossus", @@ -87,6 +91,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["frost_rune"], prestigeRequirement: 0, zoneId: "shattered_ruins", + bountyRunestones: 5, }, { id: "elder_dragon", @@ -104,6 +109,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["vorpal_sword", "dragon_scale"], prestigeRequirement: 0, zoneId: "shattered_ruins", + bountyRunestones: 7, }, // ── Shadow Marshes ──────────────────────────────────────────────────────── { @@ -122,6 +128,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 0, zoneId: "shadow_marshes", + bountyRunestones: 5, }, { id: "plague_lord", @@ -139,6 +146,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["runestone_amulet"], prestigeRequirement: 0, zoneId: "shadow_marshes", + bountyRunestones: 8, }, { id: "mud_kraken", @@ -156,6 +164,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["crystal_shard"], prestigeRequirement: 0, zoneId: "shadow_marshes", + bountyRunestones: 10, }, // ── Frozen Peaks ────────────────────────────────────────────────────────── { @@ -174,6 +183,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 0, zoneId: "frozen_peaks", + bountyRunestones: 8, }, { id: "ice_queen", @@ -191,6 +201,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["frost_crystal"], prestigeRequirement: 0, zoneId: "frozen_peaks", + bountyRunestones: 12, }, { id: "void_titan", @@ -208,6 +219,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["philosophers_stone"], prestigeRequirement: 0, zoneId: "frozen_peaks", + bountyRunestones: 15, }, // ── Volcanic Depths ─────────────────────────────────────────────────────── { @@ -226,6 +238,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["flame_lance"], prestigeRequirement: 0, zoneId: "volcanic_depths", + bountyRunestones: 12, }, { id: "magma_titan", @@ -243,6 +256,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["volcanic_plate"], prestigeRequirement: 0, zoneId: "volcanic_depths", + bountyRunestones: 18, }, { id: "phoenix_lord", @@ -260,6 +274,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["eternal_flame"], prestigeRequirement: 0, zoneId: "volcanic_depths", + bountyRunestones: 25, }, // ── Astral Void (original) ──────────────────────────────────────────────── { @@ -278,6 +293,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["astral_robe"], prestigeRequirement: 0, zoneId: "astral_void", + bountyRunestones: 20, }, { id: "cosmic_horror", @@ -295,6 +311,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["celestial_blade"], prestigeRequirement: 0, zoneId: "astral_void", + bountyRunestones: 30, }, { id: "the_devourer", @@ -312,6 +329,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["infinity_gem"], prestigeRequirement: 0, zoneId: "astral_void", + bountyRunestones: 40, }, // ── Celestial Reaches ───────────────────────────────────────────────────── { @@ -330,6 +348,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["seraph_wing"], prestigeRequirement: 6, zoneId: "celestial_reaches", + bountyRunestones: 30, }, { id: "fallen_archangel", @@ -347,6 +366,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["angels_halo"], prestigeRequirement: 7, zoneId: "celestial_reaches", + bountyRunestones: 40, }, { id: "divine_judge", @@ -364,6 +384,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 8, zoneId: "celestial_reaches", + bountyRunestones: 50, }, { id: "celestial_titan", @@ -381,6 +402,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["celestial_armour"], prestigeRequirement: 9, zoneId: "celestial_reaches", + bountyRunestones: 60, }, { id: "the_first_light", @@ -398,6 +420,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["divine_edge", "heaven_mantle"], prestigeRequirement: 10, zoneId: "celestial_reaches", + bountyRunestones: 75, }, // ── Abyssal Trench ──────────────────────────────────────────────────────── { @@ -416,6 +439,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["depth_blade"], prestigeRequirement: 9, zoneId: "abyssal_trench", + bountyRunestones: 40, }, { id: "kraken_elder", @@ -433,6 +457,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["leviathan_eye"], prestigeRequirement: 10, zoneId: "abyssal_trench", + bountyRunestones: 55, }, { id: "abyssal_colossus", @@ -450,6 +475,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["pressure_plate"], prestigeRequirement: 11, zoneId: "abyssal_trench", + bountyRunestones: 70, }, { id: "the_deep_one", @@ -467,6 +493,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 12, zoneId: "abyssal_trench", + bountyRunestones: 85, }, { id: "elder_abomination", @@ -484,6 +511,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["abyssal_edge", "abyss_shroud"], prestigeRequirement: 13, zoneId: "abyssal_trench", + bountyRunestones: 100, }, // ── Infernal Court ──────────────────────────────────────────────────────── { @@ -502,6 +530,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["demon_hide"], prestigeRequirement: 12, zoneId: "infernal_court", + bountyRunestones: 55, }, { id: "hellfire_titan", @@ -519,6 +548,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["hellfire_edge"], prestigeRequirement: 13, zoneId: "infernal_court", + bountyRunestones: 70, }, { id: "lord_of_sin", @@ -536,6 +566,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["soul_gem"], prestigeRequirement: 14, zoneId: "infernal_court", + bountyRunestones: 90, }, { id: "infernal_sovereign", @@ -553,6 +584,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 15, zoneId: "infernal_court", + bountyRunestones: 110, }, { id: "the_fallen", @@ -570,6 +602,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["infernal_edge", "sinslayer_aegis"], prestigeRequirement: 16, zoneId: "infernal_court", + bountyRunestones: 135, }, // ── Crystalline Spire ───────────────────────────────────────────────────── { @@ -588,6 +621,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["prism_blade"], prestigeRequirement: 15, zoneId: "crystalline_spire", + bountyRunestones: 70, }, { id: "crystal_drake", @@ -605,6 +639,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 16, zoneId: "crystalline_spire", + bountyRunestones: 90, }, { id: "the_faceted", @@ -622,6 +657,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["faceted_armour"], prestigeRequirement: 17, zoneId: "crystalline_spire", + bountyRunestones: 115, }, { id: "diamond_colossus", @@ -639,6 +675,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["prism_eye"], prestigeRequirement: 18, zoneId: "crystalline_spire", + bountyRunestones: 140, }, { id: "crystal_sovereign", @@ -656,6 +693,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["crystal_sovereign_blade", "diamond_plate"], prestigeRequirement: 19, zoneId: "crystalline_spire", + bountyRunestones: 175, }, // ── Void Sanctum ────────────────────────────────────────────────────────── { @@ -674,6 +712,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["void_annihilator"], prestigeRequirement: 18, zoneId: "void_sanctum", + bountyRunestones: 90, }, { id: "eternal_shade", @@ -691,6 +730,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["eternal_shroud"], prestigeRequirement: 19, zoneId: "void_sanctum", + bountyRunestones: 115, }, { id: "the_unmaker", @@ -708,6 +748,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 20, zoneId: "void_sanctum", + bountyRunestones: 145, }, { id: "void_progenitor", @@ -725,6 +766,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["void_heart_gem"], prestigeRequirement: 21, zoneId: "void_sanctum", + bountyRunestones: 180, }, { id: "void_emperor", @@ -742,6 +784,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["sanctum_breaker", "void_emperor_plate"], prestigeRequirement: 22, zoneId: "void_sanctum", + bountyRunestones: 225, }, // ── Eternal Throne ──────────────────────────────────────────────────────── { @@ -760,6 +803,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["eternal_armour"], prestigeRequirement: 21, zoneId: "eternal_throne", + bountyRunestones: 115, }, { id: "eternal_knight", @@ -777,6 +821,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["throne_blade"], prestigeRequirement: 22, zoneId: "eternal_throne", + bountyRunestones: 150, }, { id: "the_undying", @@ -794,6 +839,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 23, zoneId: "eternal_throne", + bountyRunestones: 190, }, { id: "apex_sovereign", @@ -811,6 +857,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 24, zoneId: "eternal_throne", + bountyRunestones: 235, }, { id: "the_apex", @@ -828,6 +875,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["apex_sword", "apex_plate", "eternity_stone"], prestigeRequirement: 25, zoneId: "eternal_throne", + bountyRunestones: 295, }, // ── Primordial Chaos ────────────────────────────────────────────────────── { @@ -846,6 +894,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 26, zoneId: "primordial_chaos", + bountyRunestones: 150, }, { id: "creation_engine", @@ -863,6 +912,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 27, zoneId: "primordial_chaos", + bountyRunestones: 200, }, { id: "entropy_avatar", @@ -880,6 +930,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 29, zoneId: "primordial_chaos", + bountyRunestones: 265, }, { id: "primordial_titan", @@ -897,6 +948,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["chaos_mantle", "titan_core"], prestigeRequirement: 31, zoneId: "primordial_chaos", + bountyRunestones: 350, }, // ── Infinite Expanse ────────────────────────────────────────────────────── { @@ -915,6 +967,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 33, zoneId: "infinite_expanse", + bountyRunestones: 200, }, { id: "horizon_beast", @@ -932,6 +985,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 35, zoneId: "infinite_expanse", + bountyRunestones: 265, }, { id: "infinity_construct", @@ -949,6 +1003,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 37, zoneId: "infinite_expanse", + bountyRunestones: 350, }, { id: "expanse_sovereign", @@ -966,6 +1021,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["expanse_blade", "void_armour_mk2"], prestigeRequirement: 39, zoneId: "infinite_expanse", + bountyRunestones: 465, }, // ── Reality Forge ───────────────────────────────────────────────────────── { @@ -984,6 +1040,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 41, zoneId: "reality_forge", + bountyRunestones: 265, }, { id: "reality_shaper", @@ -1001,6 +1058,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 44, zoneId: "reality_forge", + bountyRunestones: 350, }, { id: "creation_prime", @@ -1018,6 +1076,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 47, zoneId: "reality_forge", + bountyRunestones: 465, }, { id: "reality_architect", @@ -1035,6 +1094,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["cosmos_blade", "reality_plate"], prestigeRequirement: 49, zoneId: "reality_forge", + bountyRunestones: 615, }, // ── Cosmic Maelstrom ────────────────────────────────────────────────────── { @@ -1053,6 +1113,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 51, zoneId: "cosmic_maelstrom", + bountyRunestones: 350, }, { id: "force_prime", @@ -1070,6 +1131,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 54, zoneId: "cosmic_maelstrom", + bountyRunestones: 465, }, { id: "maelstrom_god", @@ -1087,6 +1149,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 57, zoneId: "cosmic_maelstrom", + bountyRunestones: 615, }, { id: "cosmic_annihilator", @@ -1104,6 +1167,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["maelstrom_edge", "cosmic_plate"], prestigeRequirement: 59, zoneId: "cosmic_maelstrom", + bountyRunestones: 815, }, // ── Primeval Sanctum ────────────────────────────────────────────────────── { @@ -1122,6 +1186,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 61, zoneId: "primeval_sanctum", + bountyRunestones: 465, }, { id: "time_elder", @@ -1139,6 +1204,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 65, zoneId: "primeval_sanctum", + bountyRunestones: 615, }, { id: "origin_beast", @@ -1156,6 +1222,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 69, zoneId: "primeval_sanctum", + bountyRunestones: 815, }, { id: "primeval_god", @@ -1173,6 +1240,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["primeval_blade", "ancient_aegis"], prestigeRequirement: 74, zoneId: "primeval_sanctum", + bountyRunestones: 1080, }, // ── The Absolute ────────────────────────────────────────────────────────── { @@ -1191,6 +1259,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 76, zoneId: "the_absolute", + bountyRunestones: 615, }, { id: "void_convergence", @@ -1208,6 +1277,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 79, zoneId: "the_absolute", + bountyRunestones: 815, }, { id: "eternal_end", @@ -1225,6 +1295,7 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: [], prestigeRequirement: 83, zoneId: "the_absolute", + bountyRunestones: 1080, }, { id: "the_absolute_one", @@ -1242,5 +1313,6 @@ export const DEFAULT_BOSSES: Boss[] = [ equipmentRewards: ["absolute_blade", "eternity_plate", "omniversal_core"], prestigeRequirement: 90, zoneId: "the_absolute", + bountyRunestones: 1430, }, ]; diff --git a/apps/api/src/data/equipment.ts b/apps/api/src/data/equipment.ts index 58df11b..f07b6d2 100644 --- a/apps/api/src/data/equipment.ts +++ b/apps/api/src/data/equipment.ts @@ -21,6 +21,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 1.25 }, owned: false, equipped: false, + setId: "iron_vanguard", }, { id: "enchanted_blade", @@ -42,6 +43,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ owned: false, equipped: false, cost: { gold: 0, essence: 500, crystals: 0 }, + setId: "shadow_infiltrator", }, { id: "flame_lance", @@ -52,6 +54,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 1.7 }, owned: false, equipped: false, + setId: "volcanic_forger", }, { id: "vorpal_sword", @@ -115,6 +118,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 1.25 }, owned: false, equipped: false, + setId: "iron_vanguard", }, { id: "hide_armour", @@ -146,6 +150,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ owned: false, equipped: false, cost: { gold: 0, essence: 400, crystals: 0 }, + setId: "shadow_infiltrator", }, { id: "volcanic_plate", @@ -156,6 +161,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 1.65, combatMultiplier: 1.15 }, owned: false, equipped: false, + setId: "volcanic_forger", }, { id: "dragon_scale", @@ -208,6 +214,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 1.25 }, owned: false, equipped: false, + setId: "iron_vanguard", }, { id: "frost_rune", @@ -248,6 +255,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 1.55, goldMultiplier: 1.1 }, owned: false, equipped: false, + setId: "volcanic_forger", }, { id: "void_compass", @@ -259,6 +267,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ owned: false, equipped: false, cost: { gold: 0, essence: 350, crystals: 0 }, + setId: "shadow_infiltrator", }, { id: "frost_crystal", @@ -310,6 +319,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 3.5 }, owned: false, equipped: false, + setId: "celestial_guardian", }, { id: "angels_halo", @@ -320,6 +330,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 2.75, goldMultiplier: 1.3 }, owned: false, equipped: false, + setId: "celestial_guardian", }, { id: "celestial_armour", @@ -330,6 +341,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 2.75 }, owned: false, equipped: false, + setId: "celestial_guardian", }, { id: "divine_edge", @@ -361,6 +373,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 4.5 }, owned: false, equipped: false, + setId: "abyssal_predator", }, { id: "leviathan_eye", @@ -371,6 +384,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 3.0, goldMultiplier: 1.35 }, owned: false, equipped: false, + setId: "abyssal_predator", }, { id: "pressure_plate", @@ -381,6 +395,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 3.25 }, owned: false, equipped: false, + setId: "abyssal_predator", }, { id: "abyssal_edge", @@ -412,6 +427,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 3.75 }, owned: false, equipped: false, + setId: "infernal_conqueror", }, { id: "hellfire_edge", @@ -422,6 +438,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 5.5 }, owned: false, equipped: false, + setId: "infernal_conqueror", }, { id: "soul_gem", @@ -432,6 +449,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 3.25, goldMultiplier: 1.4 }, owned: false, equipped: false, + setId: "infernal_conqueror", }, { id: "infernal_edge", @@ -463,6 +481,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 6.5 }, owned: false, equipped: false, + setId: "crystal_domain", }, { id: "faceted_armour", @@ -473,6 +492,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 4.5 }, owned: false, equipped: false, + setId: "crystal_domain", }, { id: "prism_eye", @@ -483,6 +503,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 3.5, goldMultiplier: 1.5 }, owned: false, equipped: false, + setId: "crystal_domain", }, { id: "crystal_sovereign_blade", @@ -514,6 +535,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 8.0 }, owned: false, equipped: false, + setId: "void_emperor", }, { id: "eternal_shroud", @@ -524,6 +546,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 5.5 }, owned: false, equipped: false, + setId: "void_emperor", }, { id: "void_heart_gem", @@ -534,6 +557,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 4.0, goldMultiplier: 1.6 }, owned: false, equipped: false, + setId: "void_emperor", }, { id: "sanctum_breaker", @@ -565,6 +589,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { goldMultiplier: 7.0 }, owned: false, equipped: false, + setId: "eternal_throne", }, { id: "throne_blade", @@ -575,6 +600,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { combatMultiplier: 10.0 }, owned: false, equipped: false, + setId: "eternal_throne", }, { id: "apex_sword", @@ -605,6 +631,7 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [ bonus: { clickMultiplier: 5.0, goldMultiplier: 2.0, combatMultiplier: 1.5 }, owned: false, equipped: false, + setId: "eternal_throne", }, // ── Purchasable endgame sinks ───────────────────────────────────────────── { diff --git a/apps/api/src/data/equipmentSets.ts b/apps/api/src/data/equipmentSets.ts new file mode 100644 index 0000000..08575f0 --- /dev/null +++ b/apps/api/src/data/equipmentSets.ts @@ -0,0 +1,94 @@ +import type { EquipmentSet } from "@elysium/types"; + +export const DEFAULT_EQUIPMENT_SETS: EquipmentSet[] = [ + { + id: "iron_vanguard", + name: "Iron Vanguard", + description: "The armaments of a seasoned guild soldier — proven steel, reliable gold.", + pieces: ["iron_sword", "chainmail", "mages_focus"], + bonuses: { + 2: { goldMultiplier: 1.1 }, + 3: { combatMultiplier: 1.1 }, + }, + }, + { + id: "shadow_infiltrator", + name: "Shadow Infiltrator", + description: "Gear forged from the Shadow Marshes themselves — unseen, unstoppable.", + pieces: ["shadow_dagger", "void_shroud", "void_compass"], + bonuses: { + 2: { goldMultiplier: 1.15 }, + 3: { clickMultiplier: 1.2 }, + }, + }, + { + id: "volcanic_forger", + name: "Volcanic Forger", + description: "Weapons and armour tempered in the depths of the Volcanic Reaches.", + pieces: ["flame_lance", "volcanic_plate", "crystal_shard"], + bonuses: { + 2: { combatMultiplier: 1.15 }, + 3: { goldMultiplier: 1.15 }, + }, + }, + { + id: "celestial_guardian", + name: "Celestial Guardian", + description: "Relics of the Celestial Reaches — divine power made manifest.", + pieces: ["seraph_wing", "celestial_armour", "angels_halo"], + bonuses: { + 2: { combatMultiplier: 1.2 }, + 3: { goldMultiplier: 1.2 }, + }, + }, + { + id: "abyssal_predator", + name: "Abyssal Predator", + description: "Trophies reclaimed from the deepest trenches of the Abyssal Reaches.", + pieces: ["depth_blade", "pressure_plate", "leviathan_eye"], + bonuses: { + 2: { goldMultiplier: 1.2 }, + 3: { clickMultiplier: 1.25 }, + }, + }, + { + id: "infernal_conqueror", + name: "Infernal Conqueror", + description: "Forged in the heart of the Infernal Court from the essence of the defeated.", + pieces: ["hellfire_edge", "demon_hide", "soul_gem"], + bonuses: { + 2: { combatMultiplier: 1.25 }, + 3: { goldMultiplier: 1.25 }, + }, + }, + { + id: "crystal_domain", + name: "Crystal Domain", + description: "Instruments of the Crystalline Spire — reality refracted into absolute efficiency.", + pieces: ["prism_blade", "faceted_armour", "prism_eye"], + bonuses: { + 2: { clickMultiplier: 1.25 }, + 3: { goldMultiplier: 1.25 }, + }, + }, + { + id: "void_emperor", + name: "Void Emperor", + description: "The regalia of the Void Sanctum's lord — power carved from absolute nothingness.", + pieces: ["void_annihilator", "eternal_shroud", "void_heart_gem"], + bonuses: { + 2: { goldMultiplier: 1.3 }, + 3: { combatMultiplier: 1.3 }, + }, + }, + { + id: "eternal_throne", + name: "Eternal Throne", + description: "The armaments of the Eternal Throne — weapons and armour that have endured all of time.", + pieces: ["throne_blade", "eternal_armour", "eternity_stone"], + bonuses: { + 2: { combatMultiplier: 1.35, goldMultiplier: 1.25 }, + 3: { clickMultiplier: 1.35 }, + }, + }, +]; diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index 42d268c..7759c12 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -1,10 +1,14 @@ import type { BossChallengeResponse, GameState } from "@elysium/types"; +import { computeSetBonuses } from "@elysium/types"; import { Hono } from "hono"; +import type { HonoEnv } from "../types/hono.js"; import { prisma } from "../db/client.js"; +import { DEFAULT_BOSSES } from "../data/bosses.js"; +import { DEFAULT_EQUIPMENT_SETS } from "../data/equipmentSets.js"; import { authMiddleware } from "../middleware/auth.js"; import { updateChallengeProgress } from "../services/dailyChallenges.js"; -export const bossRouter = new Hono(); +export const bossRouter = new Hono(); bossRouter.use("*", authMiddleware); @@ -25,6 +29,9 @@ const calculatePartyStats = ( .filter((e) => e.equipped && e.bonus.combatMultiplier != null) .reduce((mult, e) => mult * (e.bonus.combatMultiplier ?? 1), 1); + const equippedItemIds = (state.equipment ?? []).filter((e) => e.equipped).map((e) => e.id); + const setCombatMultiplier = computeSetBonuses(equippedItemIds, DEFAULT_EQUIPMENT_SETS).combatMultiplier; + let partyDPS = 0; let partyMaxHp = 0; @@ -52,7 +59,7 @@ const calculatePartyStats = ( partyMaxHp += adventurer.level * 50 * adventurer.count; } - partyDPS *= equipmentCombatMultiplier; + partyDPS *= equipmentCombatMultiplier * setCombatMultiplier; return { partyDPS, partyMaxHp }; }; @@ -185,12 +192,18 @@ bossRouter.post("/challenge", async (context) => { state.resources.crystals += crystalsAwarded; } + // First-kill bounty — look up authoritative bounty from static data + const staticBoss = DEFAULT_BOSSES.find((b) => b.id === body.bossId); + const bountyRunestones = staticBoss?.bountyRunestones ?? 0; + state.prestige.runestones += bountyRunestones; + rewards = { gold: boss.goldReward, essence: boss.essenceReward, crystals: boss.crystalReward, upgradeIds: boss.upgradeRewards, equipmentIds: equipmentRewards, + bountyRunestones, }; } else { bossHpAtBattleEnd = Math.max( diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index af83fb9..a92f64b 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -1,11 +1,14 @@ import type { GameState, SaveRequest } from "@elysium/types"; +import { computeSetBonuses } from "@elysium/types"; import { createHmac } from "node:crypto"; import { Hono } from "hono"; +import type { HonoEnv } from "../types/hono.js"; import { prisma } from "../db/client.js"; import { DEFAULT_ACHIEVEMENTS } from "../data/achievements.js"; import { DEFAULT_ADVENTURERS } from "../data/adventurers.js"; import { DEFAULT_BOSSES } from "../data/bosses.js"; import { DEFAULT_EQUIPMENT } from "../data/equipment.js"; +import { DEFAULT_EQUIPMENT_SETS } from "../data/equipmentSets.js"; import { DEFAULT_QUESTS } from "../data/quests.js"; import { authMiddleware } from "../middleware/auth.js"; import { getOrResetDailyChallenges } from "../services/dailyChallenges.js"; @@ -44,6 +47,8 @@ const computeMaxPassiveIncome = ( (mult, e) => mult * (e.bonus.goldMultiplier ?? 1), 1, ); + const equippedItemIds = equippedItems.map((e) => e.id); + const setGoldMultiplier = computeSetBonuses(equippedItemIds, DEFAULT_EQUIPMENT_SETS).goldMultiplier; const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1; const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; @@ -70,7 +75,8 @@ const computeMaxPassiveIncome = ( upgradeMultiplier * prestige * runestonesIncome * - equipmentGoldMultiplier; + equipmentGoldMultiplier * + setGoldMultiplier; essencePerSecond += adventurer.essencePerSecond * @@ -93,9 +99,11 @@ const computeMaxClickGoldPerSecond = (state: GameState): number => { .filter((u) => u.purchased && u.target === "click") .reduce((mult, u) => mult * u.multiplier, 1); - const equipmentClickMultiplier = (state.equipment ?? []) - .filter((e) => e.equipped && e.bonus.clickMultiplier != null) + const equippedItems = (state.equipment ?? []).filter((e) => e.equipped); + const equipmentClickMultiplier = equippedItems + .filter((e) => e.bonus.clickMultiplier != null) .reduce((mult, e) => mult * (e.bonus.clickMultiplier ?? 1), 1); + const setClickMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), DEFAULT_EQUIPMENT_SETS).clickMultiplier; const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1; @@ -104,7 +112,8 @@ const computeMaxClickGoldPerSecond = (state: GameState): number => { clickMultiplier * state.prestige.productionMultiplier * runestonesClick * - equipmentClickMultiplier; + equipmentClickMultiplier * + setClickMultiplier; return clickPower * CLICK_BUFFER_CPS; }; @@ -284,7 +293,7 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat return { ...incoming, resources, bosses, quests, achievements, prestige }; }; -export const gameRouter = new Hono(); +export const gameRouter = new Hono(); gameRouter.use("*", authMiddleware); @@ -610,8 +619,8 @@ gameRouter.post("/save", async (context) => { await prisma.gameState.upsert({ where: { discordId }, - create: { discordId, state: stateToSave, updatedAt: now }, - update: { state: stateToSave, updatedAt: now }, + create: { discordId, state: stateToSave as unknown as never, updatedAt: now }, + update: { state: stateToSave as unknown as never, updatedAt: now }, }); const signature = secret ? computeHmac(JSON.stringify(stateToSave), secret) : undefined; diff --git a/apps/web/src/components/game/AboutPanel.tsx b/apps/web/src/components/game/AboutPanel.tsx index 02c84d4..cf01b37 100644 --- a/apps/web/src/components/game/AboutPanel.tsx +++ b/apps/web/src/components/game/AboutPanel.tsx @@ -28,8 +28,8 @@ const HOW_TO_PLAY = [ body: "New zones unlock when you defeat the final boss AND complete the final quest of the previous zone. Each zone contains new bosses and quests with progressively greater rewards.", }, { - title: "🗡️ Equipment", - body: "Earn equipment from boss drops and quest rewards. Each piece of equipment provides bonuses to gold income, click power, or adventurer output. Rarer equipment provides stronger bonuses.", + title: "🗡️ Equipment & Sets", + body: "Earn equipment from boss drops and quest rewards. Each piece provides bonuses to gold income, click power, or combat. Rarer equipment provides stronger bonuses. Equip matching set pieces (2 or 3 of a named set) to unlock escalating set bonuses shown at the top of the Equipment panel.", }, { title: "⭐ Prestige", diff --git a/apps/web/src/components/game/BattleModal.tsx b/apps/web/src/components/game/BattleModal.tsx index b56243a..82fb252 100644 --- a/apps/web/src/components/game/BattleModal.tsx +++ b/apps/web/src/components/game/BattleModal.tsx @@ -133,6 +133,9 @@ export const BattleModal = ({ {result.rewards.crystals > 0 && ( 💎 {formatNumber(result.rewards.crystals)} crystals )} + {result.rewards.bountyRunestones > 0 && ( + 🔮 {formatNumber(result.rewards.bountyRunestones)} runestones (first kill!) + )} )} diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx index 53b3857..52082c5 100644 --- a/apps/web/src/components/game/BossPanel.tsx +++ b/apps/web/src/components/game/BossPanel.tsx @@ -70,6 +70,9 @@ const BossCard = ({ {(boss.equipmentRewards ?? []).length > 0 && ( 🗡️ {boss.equipmentRewards.length} Equipment )} + {boss.status !== "defeated" && boss.bountyRunestones > 0 && ( + 🔮 {boss.bountyRunestones} (first kill) + )} {(boss.status === "available" || boss.status === "in_progress") && ( diff --git a/apps/web/src/components/game/EquipmentPanel.tsx b/apps/web/src/components/game/EquipmentPanel.tsx index e8aa8ee..b348c68 100644 --- a/apps/web/src/components/game/EquipmentPanel.tsx +++ b/apps/web/src/components/game/EquipmentPanel.tsx @@ -1,6 +1,7 @@ import type { Equipment, EquipmentType } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { EQUIPMENT_SETS } from "../../data/equipmentSets.js"; import { LockToggle } from "../ui/LockToggle.js"; const RARITY_LABEL: Record = { @@ -36,6 +37,7 @@ interface EquipmentCardProps { essence: number; crystals: number; dropBossName?: string | undefined; + setName?: string | undefined; } const costLabel = (cost: { gold: number; essence: number; crystals: number }): string => { @@ -46,7 +48,7 @@ const costLabel = (cost: { gold: number; essence: number; crystals: number }): s return parts.join(" "); }; -const EquipmentCard = ({ item, gold, essence, crystals, dropBossName }: EquipmentCardProps): React.JSX.Element => { +const EquipmentCard = ({ item, gold, essence, crystals, dropBossName, setName }: EquipmentCardProps): React.JSX.Element => { const { equipItem, buyEquipment } = useGame(); const canAfford = item.cost @@ -63,6 +65,7 @@ const EquipmentCard = ({ item, gold, essence, crystals, dropBossName }: Equipmen

{item.description}

{bonusDescription(item)}

+ {setName && 🔗 {setName}} {!item.owned && item.cost && (

{costLabel(item.cost)}

)} @@ -121,6 +124,31 @@ export const EquipmentPanel = (): React.JSX.Element => { } } + // Build set name lookup for card badges + const setNameById = new Map( + EQUIPMENT_SETS.map((s) => [s.id, s.name]), + ); + + // Compute active set bonuses for the summary strip + const equippedItemIds = equipment.filter((e) => e.equipped).map((e) => e.id); + const activeSets = EQUIPMENT_SETS.map((set) => { + const count = set.pieces.filter((id) => equippedItemIds.includes(id)).length; + return { set, count }; + }).filter(({ count }) => count >= 2); + + const setBonusDescription = (set: typeof EQUIPMENT_SETS[number], count: number): string => { + const parts: string[] = []; + for (const threshold of [2, 3] as const) { + if (count >= threshold) { + const bonus = set.bonuses[threshold]; + if (bonus.goldMultiplier) parts.push(`+${Math.round((bonus.goldMultiplier - 1) * 100)}% Gold/s (${threshold}pc)`); + if (bonus.combatMultiplier) parts.push(`+${Math.round((bonus.combatMultiplier - 1) * 100)}% Combat (${threshold}pc)`); + if (bonus.clickMultiplier) parts.push(`+${Math.round((bonus.clickMultiplier - 1) * 100)}% Click (${threshold}pc)`); + } + } + return parts.join(", "); + }; + return (
@@ -132,9 +160,21 @@ export const EquipmentPanel = (): React.JSX.Element => { />

- Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time. + Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time. Equip matching set pieces for bonus effects!

+ {activeSets.length > 0 && ( +
+

✨ Active Set Bonuses

+ {activeSets.map(({ set, count }) => ( +
+ {set.name} ({count}/{set.pieces.length}) + {setBonusDescription(set, count)} +
+ ))} +
+ )} + {SLOT_ORDER.map((slotType) => { const items = equipment.filter( (e) => e.type === slotType && (showLocked || e.owned), @@ -151,6 +191,7 @@ export const EquipmentPanel = (): React.JSX.Element => { essence={state.resources.essence} crystals={state.resources.crystals} dropBossName={equipmentDropSources.get(item.id)} + setName={item.setId ? setNameById.get(item.setId) : undefined} /> ))} {items.length === 0 && ( diff --git a/apps/web/src/data/equipmentSets.ts b/apps/web/src/data/equipmentSets.ts new file mode 100644 index 0000000..cb6645d --- /dev/null +++ b/apps/web/src/data/equipmentSets.ts @@ -0,0 +1,94 @@ +import type { EquipmentSet } from "@elysium/types"; + +export const EQUIPMENT_SETS: EquipmentSet[] = [ + { + id: "iron_vanguard", + name: "Iron Vanguard", + description: "The armaments of a seasoned guild soldier — proven steel, reliable gold.", + pieces: ["iron_sword", "chainmail", "mages_focus"], + bonuses: { + 2: { goldMultiplier: 1.1 }, + 3: { combatMultiplier: 1.1 }, + }, + }, + { + id: "shadow_infiltrator", + name: "Shadow Infiltrator", + description: "Gear forged from the Shadow Marshes themselves — unseen, unstoppable.", + pieces: ["shadow_dagger", "void_shroud", "void_compass"], + bonuses: { + 2: { goldMultiplier: 1.15 }, + 3: { clickMultiplier: 1.2 }, + }, + }, + { + id: "volcanic_forger", + name: "Volcanic Forger", + description: "Weapons and armour tempered in the depths of the Volcanic Reaches.", + pieces: ["flame_lance", "volcanic_plate", "crystal_shard"], + bonuses: { + 2: { combatMultiplier: 1.15 }, + 3: { goldMultiplier: 1.15 }, + }, + }, + { + id: "celestial_guardian", + name: "Celestial Guardian", + description: "Relics of the Celestial Reaches — divine power made manifest.", + pieces: ["seraph_wing", "celestial_armour", "angels_halo"], + bonuses: { + 2: { combatMultiplier: 1.2 }, + 3: { goldMultiplier: 1.2 }, + }, + }, + { + id: "abyssal_predator", + name: "Abyssal Predator", + description: "Trophies reclaimed from the deepest trenches of the Abyssal Reaches.", + pieces: ["depth_blade", "pressure_plate", "leviathan_eye"], + bonuses: { + 2: { goldMultiplier: 1.2 }, + 3: { clickMultiplier: 1.25 }, + }, + }, + { + id: "infernal_conqueror", + name: "Infernal Conqueror", + description: "Forged in the heart of the Infernal Court from the essence of the defeated.", + pieces: ["hellfire_edge", "demon_hide", "soul_gem"], + bonuses: { + 2: { combatMultiplier: 1.25 }, + 3: { goldMultiplier: 1.25 }, + }, + }, + { + id: "crystal_domain", + name: "Crystal Domain", + description: "Instruments of the Crystalline Spire — reality refracted into absolute efficiency.", + pieces: ["prism_blade", "faceted_armour", "prism_eye"], + bonuses: { + 2: { clickMultiplier: 1.25 }, + 3: { goldMultiplier: 1.25 }, + }, + }, + { + id: "void_emperor", + name: "Void Emperor", + description: "The regalia of the Void Sanctum's lord — power carved from absolute nothingness.", + pieces: ["void_annihilator", "eternal_shroud", "void_heart_gem"], + bonuses: { + 2: { goldMultiplier: 1.3 }, + 3: { combatMultiplier: 1.3 }, + }, + }, + { + id: "eternal_throne", + name: "Eternal Throne", + description: "The armaments of the Eternal Throne — weapons and armour that have endured all of time.", + pieces: ["throne_blade", "eternal_armour", "eternity_stone"], + bonuses: { + 2: { combatMultiplier: 1.35, goldMultiplier: 1.25 }, + 3: { clickMultiplier: 1.35 }, + }, + }, +]; diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index a268431..ead2aa1 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -1,4 +1,6 @@ import type { Achievement, Equipment, GameState } from "@elysium/types"; +import { computeSetBonuses } from "@elysium/types"; +import { EQUIPMENT_SETS } from "../data/equipmentSets.js"; import { updateChallengeProgress } from "../utils/dailyChallenges.js"; /** @@ -57,6 +59,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => (mult, e) => mult * (e.bonus.goldMultiplier ?? 1), 1, ); + const setGoldMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), EQUIPMENT_SETS).goldMultiplier; const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1; const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; @@ -88,6 +91,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => prestige * runestonesIncome * equipmentGoldMultiplier * + setGoldMultiplier * deltaSeconds; essenceGained += @@ -228,7 +232,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => essence: newEssence, crystals: capResource(state.resources.crystals + questCrystals + challengeCrystals), }, - dailyChallenges: updatedDailyChallenges, + ...(updatedDailyChallenges !== undefined ? { dailyChallenges: updatedDailyChallenges } : {}), player: { ...state.player, totalGoldEarned: newTotalGoldEarned, @@ -271,9 +275,11 @@ export const calculateClickPower = (state: GameState): number => { .filter((u) => u.purchased && u.target === "click") .reduce((mult, upgrade) => mult * upgrade.multiplier, 1); - const equipmentClickMultiplier = (state.equipment ?? []) - .filter((e) => e.equipped && e.bonus.clickMultiplier != null) + const equippedItems = (state.equipment ?? []).filter((e) => e.equipped); + const equipmentClickMultiplier = equippedItems + .filter((e) => e.bonus.clickMultiplier != null) .reduce((mult, e) => mult * (e.bonus.clickMultiplier ?? 1), 1); + const setClickMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), EQUIPMENT_SETS).clickMultiplier; const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1; @@ -282,6 +288,7 @@ export const calculateClickPower = (state: GameState): number => { clickMultiplier * state.prestige.productionMultiplier * runestonesClick * - equipmentClickMultiplier + equipmentClickMultiplier * + setClickMultiplier ); }; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 47f0074..ecd1127 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -498,6 +498,11 @@ body { font-size: 0.9rem; } +.boss-bounty { + color: #a78bfa; + font-weight: bold; +} + .attack-button { align-self: flex-start; background: linear-gradient(135deg, #ef4444, #b91c1c); @@ -862,6 +867,11 @@ body { margin: 0.5rem 0; } +.battle-bounty { + color: #a78bfa; + font-weight: bold; +} + .dismiss-button { background: var(--colour-accent); border: none; @@ -945,6 +955,46 @@ body { margin-bottom: 1.25rem; } +.active-sets { + background: rgba(138, 43, 226, 0.08); + border: 1px solid rgba(138, 43, 226, 0.3); + border-radius: 8px; + padding: 0.75rem 1rem; + margin-bottom: 1.25rem; +} + +.active-sets-heading { + font-size: 0.9rem; + font-weight: 600; + color: #bf7fff; + margin: 0 0 0.5rem; +} + +.active-set-row { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 1rem; + align-items: baseline; + font-size: 0.82rem; + margin-bottom: 0.25rem; +} + +.active-set-name { + font-weight: 600; + color: #d4a0ff; +} + +.active-set-bonus { + color: var(--colour-text-muted); +} + +.equipment-set-badge { + display: inline-block; + font-size: 0.75rem; + color: #bf7fff; + margin-top: 0.2rem; +} + .equipment-slot-section { margin-bottom: 1.5rem; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 80bb3c3..a94c4b1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -35,6 +35,8 @@ export type { EquipmentRarity, EquipmentType, } from "./interfaces/Equipment.js"; +export type { EquipmentSet, EquipmentSetBonus } from "./interfaces/EquipmentSet.js"; +export { computeSetBonuses } from "./interfaces/EquipmentSet.js"; export type { GameState } from "./interfaces/GameState.js"; export type { Player } from "./interfaces/Player.js"; export type { PrestigeData } from "./interfaces/Prestige.js"; diff --git a/packages/types/src/interfaces/Boss.ts b/packages/types/src/interfaces/Boss.ts index f670fc0..2df28e8 100644 --- a/packages/types/src/interfaces/Boss.ts +++ b/packages/types/src/interfaces/Boss.ts @@ -23,4 +23,6 @@ export interface Boss { prestigeRequirement: number; /** Zone this boss belongs to */ zoneId: string; + /** One-time runestone bounty awarded on first-ever defeat */ + bountyRunestones: number; } diff --git a/packages/types/src/interfaces/Equipment.ts b/packages/types/src/interfaces/Equipment.ts index 723148e..4c05496 100644 --- a/packages/types/src/interfaces/Equipment.ts +++ b/packages/types/src/interfaces/Equipment.ts @@ -24,4 +24,6 @@ export interface Equipment { equipped: boolean; /** If set, this item can be purchased directly rather than obtained via boss drops */ cost?: { gold: number; essence: number; crystals: number }; + /** Equipment set this item belongs to, if any */ + setId?: string; } diff --git a/packages/types/src/interfaces/EquipmentSet.ts b/packages/types/src/interfaces/EquipmentSet.ts new file mode 100644 index 0000000..23b389d --- /dev/null +++ b/packages/types/src/interfaces/EquipmentSet.ts @@ -0,0 +1,44 @@ +export interface EquipmentSetBonus { + goldMultiplier?: number; + combatMultiplier?: number; + clickMultiplier?: number; +} + +export interface EquipmentSet { + id: string; + name: string; + description: string; + /** Equipment IDs that make up this set */ + pieces: string[]; + bonuses: { + 2: EquipmentSetBonus; + 3: EquipmentSetBonus; + }; +} + +/** + * Given a list of equipped item IDs and a set catalogue, returns the combined + * multiplicative bonuses granted by all active set bonuses. + */ +export const computeSetBonuses = ( + equippedItemIds: string[], + sets: EquipmentSet[], +): { goldMultiplier: number; combatMultiplier: number; clickMultiplier: number } => { + let goldMultiplier = 1; + let combatMultiplier = 1; + let clickMultiplier = 1; + + for (const set of sets) { + const count = set.pieces.filter((id) => equippedItemIds.includes(id)).length; + for (const threshold of [2, 3] as const) { + if (count >= threshold) { + const bonus = set.bonuses[threshold]; + goldMultiplier *= bonus.goldMultiplier ?? 1; + combatMultiplier *= bonus.combatMultiplier ?? 1; + clickMultiplier *= bonus.clickMultiplier ?? 1; + } + } + } + + return { goldMultiplier, combatMultiplier, clickMultiplier }; +}; diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index 2671502..cd8d3b0 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": ".", "declaration": true }, - "exclude": ["test/**/*.ts"] + "exclude": ["test/**/*.ts", "prod/**"] }