feat: major content expansion with essence and crystal sinks

- 6 zones (up from 3): Shadow Marshes, Volcanic Depths, Astral Void
- 18 bosses (up from 4): 3 per zone with zone-based sequential unlock
- 24 quests (up from 9): 3-4 per zone, reorganised by zone
- 15 adventurer tiers (up from 10): Shadow Assassin through Divine Champion
- 28 equipment pieces (up from 12): boss drops + purchasable with essence/crystals
- 24 upgrades (up from 13): crystal-cost upgrades + new adventurer upgrades
- 22 achievements (up from 14): new milestones for bosses, quests, gold, armies
- Purchasable equipment system: buy items directly with essence or crystals
- Crystal-cost upgrades: spend crystals on global and click power boosts
- Zone-based boss progression: defeating a zone's last boss unlocks the next zone
This commit is contained in:
2026-03-06 14:36:41 -08:00
committed by Naomi Carrigan
parent 653c36c886
commit 772d733e86
15 changed files with 1202 additions and 81 deletions
+76 -6
View File
@@ -1,6 +1,7 @@
import type { Achievement } from "@elysium/types"; import type { Achievement } from "@elysium/types";
export const DEFAULT_ACHIEVEMENTS: Achievement[] = [ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
// Click milestones
{ {
id: "first_click", id: "first_click",
name: "First Strike", name: "First Strike",
@@ -28,6 +29,16 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
reward: { crystals: 100 }, reward: { crystals: 100 },
unlockedAt: null, unlockedAt: null,
}, },
{
id: "click_legend",
name: "Click Legend",
description: "Click the Guild Hall 10,000 times.",
icon: "🌩️",
condition: { type: "totalClicks", amount: 10_000 },
reward: { crystals: 300 },
unlockedAt: null,
},
// Gold milestones
{ {
id: "first_gold", id: "first_gold",
name: "First Gold", name: "First Gold",
@@ -64,6 +75,16 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
reward: { crystals: 500 }, reward: { crystals: 500 },
unlockedAt: null, unlockedAt: null,
}, },
{
id: "trillionaire",
name: "Trillionaire",
description: "Earn 1,000,000,000,000 gold in total.",
icon: "💎",
condition: { type: "totalGoldEarned", amount: 1_000_000_000_000 },
reward: { crystals: 2_000 },
unlockedAt: null,
},
// Quest milestones
{ {
id: "first_quest", id: "first_quest",
name: "Adventurous Spirit", name: "Adventurous Spirit",
@@ -82,6 +103,16 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
reward: { crystals: 50 }, reward: { crystals: 50 },
unlockedAt: null, unlockedAt: null,
}, },
{
id: "quest_master",
name: "Quest Master",
description: "Complete 15 quests.",
icon: "🗺️",
condition: { type: "questsCompleted", amount: 15 },
reward: { crystals: 200 },
unlockedAt: null,
},
// Boss milestones
{ {
id: "boss_slayer", id: "boss_slayer",
name: "Boss Slayer", name: "Boss Slayer",
@@ -92,14 +123,33 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
unlockedAt: null, unlockedAt: null,
}, },
{ {
id: "legendary_hunter", id: "boss_veteran",
name: "Legendary Hunter", name: "Boss Veteran",
description: "Defeat all four bosses.", description: "Defeat 5 bosses.",
icon: "🏆", icon: "🗡️",
condition: { type: "bossesDefeated", amount: 4 }, condition: { type: "bossesDefeated", amount: 5 },
reward: { crystals: 200 }, reward: { crystals: 150 },
unlockedAt: null, unlockedAt: null,
}, },
{
id: "legendary_hunter",
name: "Legendary Hunter",
description: "Defeat 10 bosses.",
icon: "🏆",
condition: { type: "bossesDefeated", amount: 10 },
reward: { crystals: 500 },
unlockedAt: null,
},
{
id: "devourer_slayer",
name: "World Saver",
description: "Defeat all 18 bosses, including the Devourer of Worlds.",
icon: "🌟",
condition: { type: "bossesDefeated", amount: 18 },
reward: { crystals: 2_000 },
unlockedAt: null,
},
// Adventurer milestones
{ {
id: "guild_master", id: "guild_master",
name: "Guild Master", name: "Guild Master",
@@ -118,6 +168,16 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
reward: { crystals: 200 }, reward: { crystals: 200 },
unlockedAt: null, unlockedAt: null,
}, },
{
id: "army_legend",
name: "Legendary Commander",
description: "Recruit a total of 5,000 adventurers.",
icon: "⚜️",
condition: { type: "adventurerTotal", amount: 5_000 },
reward: { crystals: 750 },
unlockedAt: null,
},
// Prestige milestones
{ {
id: "first_prestige", id: "first_prestige",
name: "Born Again", name: "Born Again",
@@ -127,6 +187,7 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
reward: { crystals: 100 }, reward: { crystals: 100 },
unlockedAt: null, unlockedAt: null,
}, },
// Collection milestones
{ {
id: "collector", id: "collector",
name: "Collector", name: "Collector",
@@ -136,4 +197,13 @@ export const DEFAULT_ACHIEVEMENTS: Achievement[] = [
reward: { crystals: 10 }, reward: { crystals: 10 },
unlockedAt: null, unlockedAt: null,
}, },
{
id: "arsenal",
name: "Arsenal",
description: "Own 12 pieces of equipment.",
icon: "🗃️",
condition: { type: "equipmentOwned", amount: 12 },
reward: { crystals: 200 },
unlockedAt: null,
},
]; ];
+55
View File
@@ -111,4 +111,59 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [
count: 0, count: 0,
unlocked: false, unlocked: false,
}, },
{
id: "shadow_assassin",
name: "Shadow Assassin",
class: "rogue",
level: 11,
goldPerSecond: 5_000,
essencePerSecond: 6,
combatPower: 18_000,
count: 0,
unlocked: false,
},
{
id: "arcane_scholar",
name: "Arcane Scholar",
class: "mage",
level: 12,
goldPerSecond: 14_000,
essencePerSecond: 15,
combatPower: 45_000,
count: 0,
unlocked: false,
},
{
id: "void_walker",
name: "Void Walker",
class: "rogue",
level: 13,
goldPerSecond: 40_000,
essencePerSecond: 35,
combatPower: 130_000,
count: 0,
unlocked: false,
},
{
id: "celestial_guard",
name: "Celestial Guard",
class: "paladin",
level: 14,
goldPerSecond: 120_000,
essencePerSecond: 100,
combatPower: 400_000,
count: 0,
unlocked: false,
},
{
id: "divine_champion",
name: "Divine Champion",
class: "warrior",
level: 15,
goldPerSecond: 400_000,
essencePerSecond: 300,
combatPower: 1_200_000,
count: 0,
unlocked: false,
},
]; ];
+256 -12
View File
@@ -1,6 +1,7 @@
import type { Boss } from "@elysium/types"; import type { Boss } from "@elysium/types";
export const DEFAULT_BOSSES: Boss[] = [ export const DEFAULT_BOSSES: Boss[] = [
// ── Verdant Vale ──────────────────────────────────────────────────────────
{ {
id: "troll_king", id: "troll_king",
name: "The Troll King", name: "The Troll King",
@@ -35,38 +36,281 @@ export const DEFAULT_BOSSES: Boss[] = [
prestigeRequirement: 0, prestigeRequirement: 0,
zoneId: "verdant_vale", zoneId: "verdant_vale",
}, },
{
id: "forest_giant",
name: "The Forest Giant",
description:
"An ancient colossus of bark and stone who has slumbered beneath the Vale for centuries. Its awakening spells disaster for every settlement in the region.",
status: "locked",
maxHp: 35_000,
currentHp: 35_000,
damagePerSecond: 40,
goldReward: 350_000,
essenceReward: 400,
crystalReward: 20,
upgradeRewards: ["archmage_1"],
equipmentRewards: ["hide_armour", "rune_stone"],
prestigeRequirement: 0,
zoneId: "verdant_vale",
},
// ── Shattered Ruins ───────────────────────────────────────────────────────
{
id: "stone_golem",
name: "The Stone Golem",
description:
"A guardian construct from the fallen civilisation, still faithfully protecting the ruins of a city long since turned to dust.",
status: "locked",
maxHp: 60_000,
currentHp: 60_000,
damagePerSecond: 60,
goldReward: 600_000,
essenceReward: 600,
crystalReward: 25,
upgradeRewards: ["paladin_1"],
equipmentRewards: [],
prestigeRequirement: 0,
zoneId: "shattered_ruins",
},
{
id: "bone_colossus",
name: "The Bone Colossus",
description:
"Forged from the skeletons of a thousand fallen warriors by the Lich Queen's disciples. Its hollow eye sockets blaze with the same sorcery that animated them.",
status: "locked",
maxHp: 200_000,
currentHp: 200_000,
damagePerSecond: 120,
goldReward: 2_000_000,
essenceReward: 1_500,
crystalReward: 60,
upgradeRewards: ["essence_guild"],
equipmentRewards: ["frost_rune"],
prestigeRequirement: 0,
zoneId: "shattered_ruins",
},
{ {
id: "elder_dragon", id: "elder_dragon",
name: "Elder Dragon Vaeltharox", name: "Elder Dragon Vaeltharox",
description: description:
"The eldest dragon in existence, older than the kingdom itself. Even his breath can level mountains.", "The eldest dragon in existence, older than the kingdom itself. Even his breath can level mountains.",
status: "locked", status: "locked",
maxHp: 100_000, maxHp: 500_000,
currentHp: 100_000, currentHp: 500_000,
damagePerSecond: 75, damagePerSecond: 200,
goldReward: 1_000_000, goldReward: 5_000_000,
essenceReward: 1_000, essenceReward: 3_000,
crystalReward: 50, crystalReward: 100,
upgradeRewards: ["click_3"], upgradeRewards: ["click_3"],
equipmentRewards: ["vorpal_sword", "dragon_scale"], equipmentRewards: ["vorpal_sword", "dragon_scale"],
prestigeRequirement: 1, prestigeRequirement: 1,
zoneId: "shattered_ruins", zoneId: "shattered_ruins",
}, },
// ── Shadow Marshes ────────────────────────────────────────────────────────
{
id: "swamp_witch",
name: "Morgantha the Swamp Witch",
description:
"She has hexed villages for three centuries from her hut on the black water. Her curse-weaving is second to none — but so is the bounty on her head.",
status: "locked",
maxHp: 80_000,
currentHp: 80_000,
damagePerSecond: 80,
goldReward: 800_000,
essenceReward: 800,
crystalReward: 30,
upgradeRewards: ["shadow_assassin_1"],
equipmentRewards: [],
prestigeRequirement: 0,
zoneId: "shadow_marshes",
},
{
id: "plague_lord",
name: "The Plague Lord",
description:
"A bloated, rotting horror that spreads pestilence wherever it walks. Entire kingdoms have fallen to the disease it carries. Yours will not.",
status: "locked",
maxHp: 300_000,
currentHp: 300_000,
damagePerSecond: 180,
goldReward: 3_000_000,
essenceReward: 2_000,
crystalReward: 80,
upgradeRewards: ["grand_council"],
equipmentRewards: ["runestone_amulet"],
prestigeRequirement: 0,
zoneId: "shadow_marshes",
},
{
id: "mud_kraken",
name: "The Mud Kraken",
description:
"An eldritch leviathan that lurks in the deepest part of the marshes, older than the gods themselves. Its tentacles have dragged ships — and armies — into the mire.",
status: "locked",
maxHp: 800_000,
currentHp: 800_000,
damagePerSecond: 350,
goldReward: 8_000_000,
essenceReward: 4_000,
crystalReward: 150,
upgradeRewards: ["arcane_scholar_1"],
equipmentRewards: ["crystal_shard"],
prestigeRequirement: 1,
zoneId: "shadow_marshes",
},
// ── Frozen Peaks ──────────────────────────────────────────────────────────
{
id: "frost_wyrm",
name: "The Frost Wyrm",
description:
"A serpentine dragon of pure ice who has hunted the tundra for aeons. Its breath flash-freezes anything it touches, and it has never known defeat.",
status: "locked",
maxHp: 500_000,
currentHp: 500_000,
damagePerSecond: 220,
goldReward: 5_000_000,
essenceReward: 3_500,
crystalReward: 100,
upgradeRewards: ["dragon_rider_1"],
equipmentRewards: [],
prestigeRequirement: 1,
zoneId: "frozen_peaks",
},
{
id: "ice_queen",
name: "The Ice Queen",
description:
"A sorceress who made a pact with the winter itself and was transformed into something no longer mortal. She rules the Frozen Peaks from a palace of living ice.",
status: "locked",
maxHp: 1_500_000,
currentHp: 1_500_000,
damagePerSecond: 500,
goldReward: 15_000_000,
essenceReward: 8_000,
crystalReward: 250,
upgradeRewards: ["void_walker_1"],
equipmentRewards: ["frost_crystal"],
prestigeRequirement: 2,
zoneId: "frozen_peaks",
},
{ {
id: "void_titan", id: "void_titan",
name: "The Void Titan", name: "The Void Titan",
description: description:
"A creature from beyond the veil of reality, drawn by the power your guild has accumulated. It must not be allowed to exist.", "A creature from beyond the veil of reality, drawn by the power your guild has accumulated. It must not be allowed to exist.",
status: "locked", status: "locked",
maxHp: 1_000_000, maxHp: 5_000_000,
currentHp: 1_000_000, currentHp: 5_000_000,
damagePerSecond: 250, damagePerSecond: 1_200,
goldReward: 10_000_000, goldReward: 50_000_000,
essenceReward: 5_000, essenceReward: 20_000,
crystalReward: 200, crystalReward: 500,
upgradeRewards: [], upgradeRewards: [],
equipmentRewards: ["philosophers_stone"], equipmentRewards: ["philosophers_stone"],
prestigeRequirement: 3, prestigeRequirement: 3,
zoneId: "frozen_peaks", zoneId: "frozen_peaks",
}, },
// ── Volcanic Depths ───────────────────────────────────────────────────────
{
id: "fire_elemental",
name: "The Ancient Fire Elemental",
description:
"Born from the first volcanic eruption the world ever knew. It exists purely to consume, and your guild looks like the finest kindling it has seen in millennia.",
status: "locked",
maxHp: 1_000_000,
currentHp: 1_000_000,
damagePerSecond: 400,
goldReward: 10_000_000,
essenceReward: 6_000,
crystalReward: 150,
upgradeRewards: ["celestial_guard_1"],
equipmentRewards: ["flame_lance"],
prestigeRequirement: 2,
zoneId: "volcanic_depths",
},
{
id: "magma_titan",
name: "The Magma Titan",
description:
"Half-giant, half-living volcano, this colossus was created by the fire elementals to guard their greatest forge. Every strike from its fists sends shockwaves through the earth.",
status: "locked",
maxHp: 4_000_000,
currentHp: 4_000_000,
damagePerSecond: 1_000,
goldReward: 40_000_000,
essenceReward: 15_000,
crystalReward: 400,
upgradeRewards: ["crystal_resonance"],
equipmentRewards: ["volcanic_plate"],
prestigeRequirement: 3,
zoneId: "volcanic_depths",
},
{
id: "phoenix_lord",
name: "The Phoenix Lord",
description:
"The apex predator of the volcanic chain — a being of pure flame that has died and reborn itself more times than recorded history. This time, it will not rise again.",
status: "locked",
maxHp: 12_000_000,
currentHp: 12_000_000,
damagePerSecond: 2_500,
goldReward: 120_000_000,
essenceReward: 40_000,
crystalReward: 800,
upgradeRewards: ["crystal_mastery"],
equipmentRewards: ["eternal_flame"],
prestigeRequirement: 4,
zoneId: "volcanic_depths",
},
// ── Astral Void ───────────────────────────────────────────────────────────
{
id: "astral_wraith",
name: "The Astral Wraith",
description:
"A being of pure psychic energy who has haunted the space between stars since before the world was formed. It feeds on consciousness itself.",
status: "locked",
maxHp: 20_000_000,
currentHp: 20_000_000,
damagePerSecond: 4_000,
goldReward: 200_000_000,
essenceReward: 60_000,
crystalReward: 1_000,
upgradeRewards: ["divine_champion_1"],
equipmentRewards: ["astral_robe"],
prestigeRequirement: 4,
zoneId: "astral_void",
},
{
id: "cosmic_horror",
name: "The Cosmic Horror",
description:
"A god-thing from before the age of mortals. Its true form cannot be perceived without madness — what you see is a mercy granted by the universe itself.",
status: "locked",
maxHp: 75_000_000,
currentHp: 75_000_000,
damagePerSecond: 10_000,
goldReward: 750_000_000,
essenceReward: 150_000,
crystalReward: 2_500,
upgradeRewards: ["crystal_focus"],
equipmentRewards: ["celestial_blade"],
prestigeRequirement: 5,
zoneId: "astral_void",
},
{
id: "the_devourer",
name: "The Devourer of Worlds",
description:
"The end. The hunger at the heart of existence that has unmade countless realities before this one. Your guild stands between it and everything that has ever lived.",
status: "locked",
maxHp: 300_000_000,
currentHp: 300_000_000,
damagePerSecond: 30_000,
goldReward: 3_000_000_000,
essenceReward: 500_000,
crystalReward: 10_000,
upgradeRewards: [],
equipmentRewards: ["infinity_gem"],
prestigeRequirement: 6,
zoneId: "astral_void",
},
]; ];
+179 -3
View File
@@ -1,7 +1,7 @@
import type { Equipment } from "@elysium/types"; import type { Equipment } from "@elysium/types";
export const DEFAULT_EQUIPMENT: Equipment[] = [ export const DEFAULT_EQUIPMENT: Equipment[] = [
// Weapons — drop from bosses; common starts owned // ── Weapons ───────────────────────────────────────────────────────────────
{ {
id: "rusty_sword", id: "rusty_sword",
name: "Rusty Sword", name: "Rusty Sword",
@@ -32,6 +32,27 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
{
id: "shadow_dagger",
name: "Shadow Dagger",
description: "Forged in the Shadow Marshes from condensed darkness. It strikes before it is seen.",
type: "weapon",
rarity: "epic",
bonus: { combatMultiplier: 1.65 },
owned: false,
equipped: false,
cost: { gold: 0, essence: 500, crystals: 0 },
},
{
id: "flame_lance",
name: "Flame Lance",
description: "A spear tipped with a shard of the Primordial Forge's eternal fire.",
type: "weapon",
rarity: "epic",
bonus: { combatMultiplier: 1.7 },
owned: false,
equipped: false,
},
{ {
id: "vorpal_sword", id: "vorpal_sword",
name: "Vorpal Sword", name: "Vorpal Sword",
@@ -42,7 +63,39 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
// Armour — drop from bosses; common starts owned {
id: "soul_reaper",
name: "Soul Reaper",
description: "A scythe that harvests not flesh but essence itself. Every swing drains the will to resist.",
type: "weapon",
rarity: "legendary",
bonus: { combatMultiplier: 2.5 },
owned: false,
equipped: false,
cost: { gold: 0, essence: 0, crystals: 300 },
},
{
id: "celestial_blade",
name: "Celestial Blade",
description: "Forged from the heart of a dying star by the Cosmic Horror itself. Its edge exists in three realities simultaneously.",
type: "weapon",
rarity: "legendary",
bonus: { combatMultiplier: 3.0 },
owned: false,
equipped: false,
},
{
id: "void_edge",
name: "Void Edge",
description: "A blade made of compressed nothingness. It does not cut — it simply unmakes.",
type: "weapon",
rarity: "legendary",
bonus: { combatMultiplier: 2.75 },
owned: false,
equipped: false,
cost: { gold: 0, essence: 2_000, crystals: 500 },
},
// ── Armour ────────────────────────────────────────────────────────────────
{ {
id: "leather_armour", id: "leather_armour",
name: "Leather Armour", name: "Leather Armour",
@@ -63,6 +116,16 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
{
id: "hide_armour",
name: "Giant's Hide Armour",
description: "Cured hide from a Forest Giant, worked into armour that radiates primal authority.",
type: "armour",
rarity: "rare",
bonus: { goldMultiplier: 1.35 },
owned: false,
equipped: false,
},
{ {
id: "plate_armour", id: "plate_armour",
name: "Plate Armour", name: "Plate Armour",
@@ -73,6 +136,27 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
{
id: "void_shroud",
name: "Void Shroud",
description: "A cloak woven from the fabric of the Shadow Marshes itself. Wealth flows to those hidden from sight.",
type: "armour",
rarity: "epic",
bonus: { goldMultiplier: 1.75 },
owned: false,
equipped: false,
cost: { gold: 0, essence: 400, crystals: 0 },
},
{
id: "volcanic_plate",
name: "Volcanic Plate",
description: "Armour quenched in magma that hardened into something neither metal nor stone. Burns with inner heat.",
type: "armour",
rarity: "epic",
bonus: { goldMultiplier: 1.65, combatMultiplier: 1.15 },
owned: false,
equipped: false,
},
{ {
id: "dragon_scale", id: "dragon_scale",
name: "Dragon Scale Armour", name: "Dragon Scale Armour",
@@ -83,7 +167,28 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
// Trinkets — drop from bosses; common starts owned {
id: "titan_aegis",
name: "Titan's Aegis",
description: "A shield-armour hybrid blessed by the celestials. Its bearer becomes a fortress.",
type: "armour",
rarity: "legendary",
bonus: { goldMultiplier: 2.5 },
owned: false,
equipped: false,
cost: { gold: 0, essence: 0, crystals: 250 },
},
{
id: "astral_robe",
name: "Astral Robe",
description: "Woven from threads of pure starlight harvested by the Astral Wraith. Income flows like cosmic energy.",
type: "armour",
rarity: "legendary",
bonus: { goldMultiplier: 2.25 },
owned: false,
equipped: false,
},
// ── Trinkets ──────────────────────────────────────────────────────────────
{ {
id: "lucky_coin", id: "lucky_coin",
name: "Lucky Coin", name: "Lucky Coin",
@@ -104,6 +209,16 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
{
id: "frost_rune",
name: "Frost Rune",
description: "A rune carved from bone-ice by the Bone Colossus. It amplifies strikes with cold precision.",
type: "trinket",
rarity: "rare",
bonus: { clickMultiplier: 1.3 },
owned: false,
equipped: false,
},
{ {
id: "arcane_orb", id: "arcane_orb",
name: "Arcane Orb", name: "Arcane Orb",
@@ -114,6 +229,47 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
{
id: "runestone_amulet",
name: "Runestone Amulet",
description: "An amulet carved from ancient runestones found in the plague ruins. Its inscriptions hum with forgotten power.",
type: "trinket",
rarity: "epic",
bonus: { clickMultiplier: 1.45, goldMultiplier: 1.15 },
owned: false,
equipped: false,
},
{
id: "crystal_shard",
name: "Crystal Shard",
description: "A fragment of the Mud Kraken's crystallised essence. Focuses raw power into devastating strikes.",
type: "trinket",
rarity: "epic",
bonus: { clickMultiplier: 1.55, goldMultiplier: 1.1 },
owned: false,
equipped: false,
},
{
id: "void_compass",
name: "Void Compass",
description: "A compass that points not north but toward the greatest concentration of power — wherever that may be.",
type: "trinket",
rarity: "epic",
bonus: { clickMultiplier: 1.6 },
owned: false,
equipped: false,
cost: { gold: 0, essence: 350, crystals: 0 },
},
{
id: "frost_crystal",
name: "Frost Crystal",
description: "A perfectly formed crystal harvested from the Ice Queen's throne room. Cold enough to burn.",
type: "trinket",
rarity: "legendary",
bonus: { clickMultiplier: 2.0, goldMultiplier: 1.2 },
owned: false,
equipped: false,
},
{ {
id: "philosophers_stone", id: "philosophers_stone",
name: "Philosopher's Stone", name: "Philosopher's Stone",
@@ -124,4 +280,24 @@ export const DEFAULT_EQUIPMENT: Equipment[] = [
owned: false, owned: false,
equipped: false, equipped: false,
}, },
{
id: "eternal_flame",
name: "Eternal Flame",
description: "A flame that has never been extinguished, sealed in crystal by the Phoenix Lord. It burns with the power of rebirth.",
type: "trinket",
rarity: "legendary",
bonus: { clickMultiplier: 2.25, goldMultiplier: 1.15 },
owned: false,
equipped: false,
},
{
id: "infinity_gem",
name: "Infinity Gem",
description: "A gem that contains a universe within it. Those who hold it become more than mortal.",
type: "trinket",
rarity: "legendary",
bonus: { clickMultiplier: 2.5, goldMultiplier: 1.3, combatMultiplier: 1.25 },
owned: false,
equipped: false,
},
]; ];
+246 -33
View File
@@ -1,6 +1,7 @@
import type { Quest } from "@elysium/types"; import type { Quest } from "@elysium/types";
export const DEFAULT_QUESTS: Quest[] = [ export const DEFAULT_QUESTS: Quest[] = [
// ── Verdant Vale ──────────────────────────────────────────────────────────
{ {
id: "first_steps", id: "first_steps",
name: "First Steps", name: "First Steps",
@@ -37,21 +38,6 @@ export const DEFAULT_QUESTS: Quest[] = [
prerequisiteIds: ["goblin_camp"], prerequisiteIds: ["goblin_camp"],
zoneId: "verdant_vale", zoneId: "verdant_vale",
}, },
{
id: "necromancer_tower",
name: "Necromancer's Tower",
description:
"A rogue necromancer has raised an army of skeletons near the city. Silence him before the dead overrun us.",
status: "locked",
durationSeconds: 25 * 60,
rewards: [
{ type: "gold", amount: 15_000 },
{ type: "essence", amount: 20 },
{ type: "upgrade", targetId: "cleric_1" },
],
prerequisiteIds: ["haunted_mine"],
zoneId: "verdant_vale",
},
{ {
id: "ancient_ruins", id: "ancient_ruins",
name: "Ancient Ruins", name: "Ancient Ruins",
@@ -65,6 +51,68 @@ export const DEFAULT_QUESTS: Quest[] = [
prerequisiteIds: ["haunted_mine"], prerequisiteIds: ["haunted_mine"],
zoneId: "verdant_vale", zoneId: "verdant_vale",
}, },
// ── Shattered Ruins ───────────────────────────────────────────────────────
{
id: "necromancer_tower",
name: "Necromancer's Tower",
description:
"A rogue necromancer has raised an army of skeletons near the city. Silence him before the dead overrun us.",
status: "locked",
durationSeconds: 25 * 60,
rewards: [
{ type: "gold", amount: 15_000 },
{ type: "essence", amount: 20 },
{ type: "upgrade", targetId: "cleric_1" },
],
prerequisiteIds: [],
zoneId: "shattered_ruins",
},
{
id: "crumbling_fortress",
name: "The Crumbling Fortress",
description:
"An ancient fortress still garrisoned by constructs who don't know the war ended. Clear it out and claim its vaults.",
status: "locked",
durationSeconds: 45 * 60,
rewards: [
{ type: "gold", amount: 80_000 },
{ type: "essence", amount: 120 },
{ type: "upgrade", targetId: "scout_1" },
],
prerequisiteIds: ["necromancer_tower"],
zoneId: "shattered_ruins",
},
{
id: "cursed_library",
name: "The Cursed Library",
description:
"A vast library sealed for centuries whose contents have warped and grown hostile. The knowledge within is priceless.",
status: "locked",
durationSeconds: 60 * 60,
rewards: [
{ type: "essence", amount: 300 },
{ type: "crystals", amount: 30 },
{ type: "upgrade", targetId: "mage_1" },
],
prerequisiteIds: ["crumbling_fortress"],
zoneId: "shattered_ruins",
},
{
id: "dragon_lair",
name: "Dragon's Lair",
description:
"The legendary lair of Pyraxis the Undying. Few who enter return — those who do are rich beyond imagining.",
status: "locked",
durationSeconds: 90 * 60,
rewards: [
{ type: "gold", amount: 500_000 },
{ type: "crystals", amount: 50 },
{ type: "adventurer", targetId: "dragon_rider" },
],
prerequisiteIds: ["cursed_library"],
zoneId: "shattered_ruins",
},
// ── Shadow Marshes ────────────────────────────────────────────────────────
{ {
id: "shadow_mere", id: "shadow_mere",
name: "The Shadow Mere", name: "The Shadow Mere",
@@ -74,26 +122,56 @@ export const DEFAULT_QUESTS: Quest[] = [
durationSeconds: 45 * 60, durationSeconds: 45 * 60,
rewards: [ rewards: [
{ type: "essence", amount: 150 }, { type: "essence", amount: 150 },
{ type: "upgrade", targetId: "scout_1" }, { type: "upgrade", targetId: "militia_1" },
], ],
prerequisiteIds: ["ancient_ruins"], prerequisiteIds: [],
zoneId: "shattered_ruins", zoneId: "shadow_marshes",
}, },
{ {
id: "dragon_lair", id: "witch_coven",
name: "Dragon's Lair", name: "The Witch Coven",
description: description:
"The legendary lair of Pyraxis the Undying. Few who enter return — those who do are rich beyond imagining.", "Deep in the marshes, a coven of swamp witches performs rites that twist the very land. Their power must be broken.",
status: "locked", status: "locked",
durationSeconds: 60 * 60, durationSeconds: 90 * 60,
rewards: [ rewards: [
{ type: "gold", amount: 500_000 }, { type: "essence", amount: 500 },
{ type: "crystals", amount: 50 }, { type: "adventurer", targetId: "shadow_assassin" },
{ type: "adventurer", targetId: "dragon_rider" },
], ],
prerequisiteIds: ["ancient_ruins"], prerequisiteIds: ["shadow_mere"],
zoneId: "shattered_ruins", zoneId: "shadow_marshes",
}, },
{
id: "sunken_temple",
name: "The Sunken Temple",
description:
"An ancient temple half-submerged in black water, its altars still humming with the power of a god long since departed.",
status: "locked",
durationSeconds: 2 * 60 * 60,
rewards: [
{ type: "gold", amount: 2_000_000 },
{ type: "crystals", amount: 75 },
{ type: "upgrade", targetId: "knight_1" },
],
prerequisiteIds: ["witch_coven"],
zoneId: "shadow_marshes",
},
{
id: "plague_ruins",
name: "The Plague Ruins",
description:
"A city that died overnight, its streets still thick with something no healer can identify. Treasures lie unclaimed among the bones.",
status: "locked",
durationSeconds: 3 * 60 * 60,
rewards: [
{ type: "gold", amount: 8_000_000 },
{ type: "essence", amount: 2_000 },
{ type: "crystals", amount: 150 },
],
prerequisiteIds: ["sunken_temple"],
zoneId: "shadow_marshes",
},
// ── Frozen Peaks ──────────────────────────────────────────────────────────
{ {
id: "frozen_wastes", id: "frozen_wastes",
name: "The Frozen Wastes", name: "The Frozen Wastes",
@@ -102,13 +180,104 @@ export const DEFAULT_QUESTS: Quest[] = [
status: "locked", status: "locked",
durationSeconds: 2 * 60 * 60, durationSeconds: 2 * 60 * 60,
rewards: [ rewards: [
{ type: "gold", amount: 2_000_000 }, { type: "gold", amount: 5_000_000 },
{ type: "crystals", amount: 150 }, { type: "crystals", amount: 100 },
{ type: "upgrade", targetId: "global_3" }, { type: "upgrade", targetId: "global_3" },
], ],
prerequisiteIds: ["dragon_lair"], prerequisiteIds: [],
zoneId: "frozen_peaks", zoneId: "frozen_peaks",
}, },
{
id: "ice_caves",
name: "The Ice Caves",
description:
"A labyrinthine network of crystal caverns that descend for miles. The cold here is a presence, not just a temperature.",
status: "locked",
durationSeconds: 3 * 60 * 60,
rewards: [
{ type: "essence", amount: 5_000 },
{ type: "crystals", amount: 200 },
{ type: "adventurer", targetId: "arcane_scholar" },
],
prerequisiteIds: ["frozen_wastes"],
zoneId: "frozen_peaks",
},
{
id: "storm_citadel",
name: "The Storm Citadel",
description:
"A fortress suspended in a permanent blizzard, built by a mage who wanted to be left alone — and succeeded for three hundred years.",
status: "locked",
durationSeconds: 5 * 60 * 60,
rewards: [
{ type: "gold", amount: 30_000_000 },
{ type: "essence", amount: 10_000 },
{ type: "upgrade", targetId: "peasant_1" },
],
prerequisiteIds: ["ice_caves"],
zoneId: "frozen_peaks",
},
// ── Volcanic Depths ───────────────────────────────────────────────────────
{
id: "lava_flows",
name: "The Lava Flows",
description:
"A river of molten rock that flows without end through the volcanic tunnels. Something valuable gleams in the depths.",
status: "locked",
durationSeconds: 3 * 60 * 60,
rewards: [
{ type: "gold", amount: 15_000_000 },
{ type: "essence", amount: 4_000 },
],
prerequisiteIds: [],
zoneId: "volcanic_depths",
},
{
id: "fire_temple",
name: "The Temple of the Flame",
description:
"A vast shrine where fire elementals perform rituals that shake the mountains. Whatever they worship, it has answered.",
status: "locked",
durationSeconds: 5 * 60 * 60,
rewards: [
{ type: "essence", amount: 12_000 },
{ type: "crystals", amount: 300 },
{ type: "adventurer", targetId: "void_walker" },
],
prerequisiteIds: ["lava_flows"],
zoneId: "volcanic_depths",
},
{
id: "magma_caverns",
name: "The Magma Caverns",
description:
"Kilometres of tunnels filled with rivers of fire and creatures born from the earth's core. The heat alone should kill you. Somehow, it won't.",
status: "locked",
durationSeconds: 7 * 60 * 60,
rewards: [
{ type: "gold", amount: 100_000_000 },
{ type: "essence", amount: 25_000 },
{ type: "crystals", amount: 600 },
],
prerequisiteIds: ["fire_temple"],
zoneId: "volcanic_depths",
},
{
id: "the_forge",
name: "The Primordial Forge",
description:
"The oldest forge in existence, where the fire elementals crafted weapons for gods. Its secrets could revolutionise your guild's arsenal.",
status: "locked",
durationSeconds: 10 * 60 * 60,
rewards: [
{ type: "gold", amount: 500_000_000 },
{ type: "essence", amount: 80_000 },
{ type: "adventurer", targetId: "celestial_guard" },
],
prerequisiteIds: ["magma_caverns"],
zoneId: "volcanic_depths",
},
// ── Astral Void ───────────────────────────────────────────────────────────
{ {
id: "void_rift", id: "void_rift",
name: "Void Rift", name: "Void Rift",
@@ -119,9 +288,53 @@ export const DEFAULT_QUESTS: Quest[] = [
rewards: [ rewards: [
{ type: "crystals", amount: 500 }, { type: "crystals", amount: 500 },
{ type: "essence", amount: 5_000 }, { type: "essence", amount: 5_000 },
{ type: "upgrade", targetId: "knight_1" },
], ],
prerequisiteIds: ["frozen_wastes"], prerequisiteIds: [],
zoneId: "frozen_peaks", zoneId: "astral_void",
},
{
id: "star_graveyard",
name: "The Star Graveyard",
description:
"A field of dead stars, each one larger than a planet, each one cold and silent where once they burned with the light of creation.",
status: "locked",
durationSeconds: 8 * 60 * 60,
rewards: [
{ type: "gold", amount: 1_000_000_000 },
{ type: "essence", amount: 100_000 },
{ type: "crystals", amount: 1_000 },
],
prerequisiteIds: ["void_rift"],
zoneId: "astral_void",
},
{
id: "between_worlds",
name: "Between Worlds",
description:
"The space between realities, where the rules that govern your world do not apply. Time is meaningless here. Power is everything.",
status: "locked",
durationSeconds: 12 * 60 * 60,
rewards: [
{ type: "essence", amount: 250_000 },
{ type: "crystals", amount: 2_000 },
{ type: "adventurer", targetId: "divine_champion" },
],
prerequisiteIds: ["star_graveyard"],
zoneId: "astral_void",
},
{
id: "the_end",
name: "The End of All Things",
description:
"There is nothing beyond this point. Only the greatest guild in the history of all existence could reach here — and you have.",
status: "locked",
durationSeconds: 24 * 60 * 60,
rewards: [
{ type: "gold", amount: 10_000_000_000 },
{ type: "essence", amount: 1_000_000 },
{ type: "crystals", amount: 10_000 },
],
prerequisiteIds: ["between_worlds"],
zoneId: "astral_void",
}, },
]; ];
+180 -4
View File
@@ -1,7 +1,7 @@
import type { Upgrade } from "@elysium/types"; import type { Upgrade } from "@elysium/types";
export const DEFAULT_UPGRADES: Upgrade[] = [ export const DEFAULT_UPGRADES: Upgrade[] = [
// Click upgrades // ── Click upgrades ────────────────────────────────────────────────────────
{ {
id: "click_1", id: "click_1",
name: "Keen Eye", name: "Keen Eye",
@@ -10,6 +10,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 100, costGold: 100,
costEssence: 0, costEssence: 0,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: true, unlocked: true,
}, },
@@ -19,8 +20,9 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
description: "Years of combat sharpen your instincts. Doubles click power again.", description: "Years of combat sharpen your instincts. Doubles click power again.",
target: "click", target: "click",
multiplier: 2, multiplier: 2,
costGold: 1000, costGold: 1_000,
costEssence: 0, costEssence: 0,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -32,10 +34,23 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 3, multiplier: 3,
costGold: 50_000, costGold: 50_000,
costEssence: 10, costEssence: 10,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
// Global upgrades {
id: "crystal_focus",
name: "Crystal Focus",
description: "Channel crystallised power into every strike. Doubles click power.",
target: "click",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 100,
purchased: false,
unlocked: true,
},
// ── Global gold upgrades ──────────────────────────────────────────────────
{ {
id: "global_1", id: "global_1",
name: "Guild Charter", name: "Guild Charter",
@@ -44,6 +59,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 1.25, multiplier: 1.25,
costGold: 500, costGold: 500,
costEssence: 0, costEssence: 0,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -55,6 +71,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 1.5, multiplier: 1.5,
costGold: 10_000, costGold: 10_000,
costEssence: 5, costEssence: 5,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -66,10 +83,59 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 1_000_000, costGold: 1_000_000,
costEssence: 100, costEssence: 100,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
// Adventurer-specific upgrades {
id: "essence_guild",
name: "Essence Guild",
description: "Forge partnerships with mage guilds across the realm. All income +50%.",
target: "global",
multiplier: 1.5,
costGold: 50_000,
costEssence: 50,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "grand_council",
name: "Grand Council",
description: "A council of the realm's greatest minds organises your operations. All income doubled.",
target: "global",
multiplier: 2,
costGold: 500_000,
costEssence: 250,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "crystal_resonance",
name: "Crystal Resonance",
description: "Align crystalline frequencies across your guild. All income +50%.",
target: "global",
multiplier: 1.5,
costGold: 0,
costEssence: 0,
costCrystals: 250,
purchased: false,
unlocked: false,
},
{
id: "crystal_mastery",
name: "Crystal Mastery",
description: "Master the art of crystal amplification. All income doubled.",
target: "global",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 600,
purchased: false,
unlocked: false,
},
// ── Adventurer-specific upgrades ──────────────────────────────────────────
{ {
id: "peasant_1", id: "peasant_1",
name: "Better Tools", name: "Better Tools",
@@ -79,6 +145,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 200, costGold: 200,
costEssence: 0, costEssence: 0,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -91,6 +158,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 1_000, costGold: 1_000,
costEssence: 0, costEssence: 0,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -103,6 +171,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 5_000, costGold: 5_000,
costEssence: 2, costEssence: 2,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -115,6 +184,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 8_000, costGold: 8_000,
costEssence: 3, costEssence: 3,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -127,6 +197,7 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 15_000, costGold: 15_000,
costEssence: 5, costEssence: 5,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
@@ -139,6 +210,111 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
multiplier: 2, multiplier: 2,
costGold: 50_000, costGold: 50_000,
costEssence: 10, costEssence: 10,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "archmage_1",
name: "Leyline Binding",
description: "Tap into the world's leylines to double archmage output.",
target: "adventurer",
adventurerId: "archmage",
multiplier: 2,
costGold: 100_000,
costEssence: 75,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "paladin_1",
name: "Holy Vanguard",
description: "Divine blessings from the gods themselves double paladin output.",
target: "adventurer",
adventurerId: "paladin",
multiplier: 2,
costGold: 200_000,
costEssence: 150,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "dragon_rider_1",
name: "Bond of Wings",
description: "The unbreakable bond between rider and dragon doubles their combined output.",
target: "adventurer",
adventurerId: "dragon_rider",
multiplier: 2,
costGold: 500_000,
costEssence: 200,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "shadow_assassin_1",
name: "Shadow Arts",
description: "Mastery of the shadow arts doubles assassin effectiveness.",
target: "adventurer",
adventurerId: "shadow_assassin",
multiplier: 2,
costGold: 0,
costEssence: 50,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "arcane_scholar_1",
name: "Ancient Tomes",
description: "Access to forbidden libraries doubles scholar output.",
target: "adventurer",
adventurerId: "arcane_scholar",
multiplier: 2,
costGold: 0,
costEssence: 150,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "void_walker_1",
name: "Void Step",
description: "Walking through the void itself doubles the output of your void walkers.",
target: "adventurer",
adventurerId: "void_walker",
multiplier: 2,
costGold: 0,
costEssence: 300,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "celestial_guard_1",
name: "Divine Ward",
description: "A blessing from the celestials themselves doubles guard output.",
target: "adventurer",
adventurerId: "celestial_guard",
multiplier: 2,
costGold: 0,
costEssence: 750,
costCrystals: 0,
purchased: false,
unlocked: false,
},
{
id: "divine_champion_1",
name: "Champion's Oath",
description: "An unbreakable oath to the divine doubles champion output.",
target: "adventurer",
adventurerId: "divine_champion",
multiplier: 2,
costGold: 0,
costEssence: 2_000,
costCrystals: 0,
purchased: false, purchased: false,
unlocked: false, unlocked: false,
}, },
+27
View File
@@ -19,6 +19,15 @@ export const DEFAULT_ZONES: Zone[] = [
status: "locked", status: "locked",
unlockBossId: "lich_queen", unlockBossId: "lich_queen",
}, },
{
id: "shadow_marshes",
name: "The Shadow Marshes",
description:
"A vast, fog-choked wetland where the sun never fully rises. Dark magic seeps from the earth itself, and things far older than the kingdom lurk beneath the murky waters.",
emoji: "🌑",
status: "locked",
unlockBossId: "troll_king",
},
{ {
id: "frozen_peaks", id: "frozen_peaks",
name: "The Frozen Peaks", name: "The Frozen Peaks",
@@ -28,4 +37,22 @@ export const DEFAULT_ZONES: Zone[] = [
status: "locked", status: "locked",
unlockBossId: "elder_dragon", unlockBossId: "elder_dragon",
}, },
{
id: "volcanic_depths",
name: "The Volcanic Depths",
description:
"A chain of active volcanoes whose caverns plunge deep into the earth's molten heart. Legendary forges burn here, tended by fire elementals who serve no master — yet.",
emoji: "🌋",
status: "locked",
unlockBossId: "bone_colossus",
},
{
id: "astral_void",
name: "The Astral Void",
description:
"Beyond the veil of the mortal world lies a realm of pure possibility and absolute terror. Stars are born and die here in moments, and the beings that call this place home have never known mortality.",
emoji: "🌌",
status: "locked",
unlockBossId: "void_titan",
},
]; ];
+13 -5
View File
@@ -146,16 +146,24 @@ bossRouter.post("/challenge", async (context) => {
} }
} }
const bossIndex = state.bosses.findIndex((b) => b.id === body.bossId); // Unlock next boss in the same zone (zone-based sequential progression)
const nextBoss = state.bosses[bossIndex + 1]; const zoneBosses = state.bosses.filter((b) => b.zoneId === boss.zoneId);
if (nextBoss && nextBoss.prestigeRequirement <= state.prestige.count) { const zoneIndex = zoneBosses.findIndex((b) => b.id === body.bossId);
nextBoss.status = "available"; const nextZoneBoss = zoneBosses[zoneIndex + 1];
if (nextZoneBoss && nextZoneBoss.prestigeRequirement <= state.prestige.count) {
const nextBossInState = state.bosses.find((b) => b.id === nextZoneBoss.id);
if (nextBossInState) nextBossInState.status = "available";
} }
// Unlock any zone whose unlock condition is this boss // Unlock any zone whose unlock condition is this boss, and activate its first boss
for (const zone of (state.zones ?? [])) { for (const zone of (state.zones ?? [])) {
if (zone.unlockBossId === body.bossId) { if (zone.unlockBossId === body.bossId) {
zone.status = "unlocked"; zone.status = "unlocked";
const newZoneBosses = state.bosses.filter((b) => b.zoneId === zone.id);
const firstNewBoss = newZoneBosses[0];
if (firstNewBoss && firstNewBoss.prestigeRequirement <= state.prestige.count) {
firstNewBoss.status = "available";
}
} }
} }
+57 -4
View File
@@ -69,7 +69,7 @@ gameRouter.get("/load", async (context) => {
} }
} }
// Backfill new quests and upgrades from defaults (add missing ones) // Backfill new quests, upgrades, zones, and bosses from defaults (add missing ones)
const { DEFAULT_QUESTS } = await import("../data/quests.js"); const { DEFAULT_QUESTS } = await import("../data/quests.js");
const { DEFAULT_UPGRADES } = await import("../data/upgrades.js"); const { DEFAULT_UPGRADES } = await import("../data/upgrades.js");
const { DEFAULT_ZONES } = await import("../data/zones.js"); const { DEFAULT_ZONES } = await import("../data/zones.js");
@@ -82,10 +82,14 @@ gameRouter.get("/load", async (context) => {
} }
} }
// Backfill zoneId on quests that predate the field // Sync zoneId on quests to match current defaults
for (const quest of state.quests) { for (const quest of state.quests) {
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
if (defaults && quest.zoneId !== defaults.zoneId) {
quest.zoneId = defaults.zoneId;
needsBackfill = true;
}
if (!quest.zoneId) { if (!quest.zoneId) {
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
quest.zoneId = defaults?.zoneId ?? "verdant_vale"; quest.zoneId = defaults?.zoneId ?? "verdant_vale";
needsBackfill = true; needsBackfill = true;
} }
@@ -98,7 +102,23 @@ gameRouter.get("/load", async (context) => {
} }
} }
// Backfill zones on saves that predate the feature // Backfill costCrystals on upgrades that predate the field
for (const upgrade of state.upgrades) {
if (upgrade.costCrystals == null) {
upgrade.costCrystals = 0;
needsBackfill = true;
}
}
// Merge new adventurers from defaults
for (const defaultAdventurer of DEFAULT_ADVENTURERS) {
if (!state.adventurers.some((a) => a.id === defaultAdventurer.id)) {
state.adventurers.push(structuredClone(defaultAdventurer));
needsBackfill = true;
}
}
// Backfill zones
if (!Array.isArray(state.zones) || state.zones.length === 0) { if (!Array.isArray(state.zones) || state.zones.length === 0) {
state.zones = structuredClone(DEFAULT_ZONES); state.zones = structuredClone(DEFAULT_ZONES);
// Infer unlock state from defeated bosses // Infer unlock state from defeated bosses
@@ -111,6 +131,22 @@ gameRouter.get("/load", async (context) => {
} }
} }
needsBackfill = true; needsBackfill = true;
} else {
// Merge new zones from defaults
for (const defaultZone of DEFAULT_ZONES) {
if (!state.zones.some((z) => z.id === defaultZone.id)) {
const newZone = structuredClone(defaultZone);
// Infer unlock state from defeated bosses
if (newZone.unlockBossId != null) {
const unlockBoss = state.bosses.find((b) => b.id === newZone.unlockBossId);
if (unlockBoss?.status === "defeated") {
newZone.status = "unlocked";
}
}
state.zones.push(newZone);
needsBackfill = true;
}
}
} }
// Backfill zoneId on bosses that predate the field // Backfill zoneId on bosses that predate the field
@@ -122,6 +158,23 @@ gameRouter.get("/load", async (context) => {
} }
} }
// Merge new bosses from defaults (new zones' bosses)
for (const defaultBoss of DEFAULT_BOSSES) {
if (!state.bosses.some((b) => b.id === defaultBoss.id)) {
const newBoss = structuredClone(defaultBoss);
// If the zone for this boss is already unlocked, make the first boss in that zone available
const zone = state.zones.find((z) => z.id === newBoss.zoneId);
if (zone?.status === "unlocked") {
const zoneBossesInState = state.bosses.filter((b) => b.zoneId === newBoss.zoneId);
if (zoneBossesInState.length === 0 && newBoss.status === "locked") {
newBoss.status = "available";
}
}
state.bosses.push(newBoss);
needsBackfill = true;
}
}
const now = Date.now(); const now = Date.now();
const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now); const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now);
@@ -32,10 +32,25 @@ const bonusDescription = (item: Equipment): string => {
interface EquipmentCardProps { interface EquipmentCardProps {
item: Equipment; item: Equipment;
gold: number;
essence: number;
crystals: number;
} }
const EquipmentCard = ({ item }: EquipmentCardProps): React.JSX.Element => { const costLabel = (cost: { gold: number; essence: number; crystals: number }): string => {
const { equipItem } = useGame(); const parts: string[] = [];
if (cost.gold > 0) parts.push(`🪙 ${cost.gold.toLocaleString()}`);
if (cost.essence > 0) parts.push(`${cost.essence.toLocaleString()}`);
if (cost.crystals > 0) parts.push(`💎 ${cost.crystals.toLocaleString()}`);
return parts.join(" ");
};
const EquipmentCard = ({ item, gold, essence, crystals }: EquipmentCardProps): React.JSX.Element => {
const { equipItem, buyEquipment } = useGame();
const canAfford = item.cost
? gold >= item.cost.gold && essence >= item.cost.essence && crystals >= item.cost.crystals
: false;
return ( return (
<div className={`equipment-card rarity-${item.rarity} ${item.equipped ? "equipped" : ""} ${!item.owned ? "not-owned" : ""}`}> <div className={`equipment-card rarity-${item.rarity} ${item.equipped ? "equipped" : ""} ${!item.owned ? "not-owned" : ""}`}>
@@ -47,9 +62,22 @@ const EquipmentCard = ({ item }: EquipmentCardProps): React.JSX.Element => {
</div> </div>
<p className="equipment-description">{item.description}</p> <p className="equipment-description">{item.description}</p>
<p className="equipment-bonus">{bonusDescription(item)}</p> <p className="equipment-bonus">{bonusDescription(item)}</p>
{!item.owned && item.cost && (
<p className="equipment-cost">{costLabel(item.cost)}</p>
)}
</div> </div>
<div className="equipment-action"> <div className="equipment-action">
{!item.owned && <span className="equipment-locked">🔒 Not yet obtained</span>} {!item.owned && !item.cost && <span className="equipment-locked">🔒 Boss drop</span>}
{!item.owned && item.cost && (
<button
className="equip-button"
disabled={!canAfford}
onClick={() => { buyEquipment(item.id); }}
type="button"
>
{canAfford ? "Purchase" : "Can't afford"}
</button>
)}
{item.owned && item.equipped && <span className="equipment-equipped-badge"> Equipped</span>} {item.owned && item.equipped && <span className="equipment-equipped-badge"> Equipped</span>}
{item.owned && !item.equipped && ( {item.owned && !item.equipped && (
<button <button
@@ -104,7 +132,13 @@ export const EquipmentPanel = (): React.JSX.Element => {
<h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3> <h3 className="slot-heading">{SLOT_LABEL[slotType]}</h3>
<div className="equipment-list"> <div className="equipment-list">
{items.map((item) => ( {items.map((item) => (
<EquipmentCard key={item.id} item={item} /> <EquipmentCard
key={item.id}
item={item}
gold={state.resources.gold}
essence={state.resources.essence}
crystals={state.resources.crystals}
/>
))} ))}
{items.length === 0 && ( {items.length === 0 && (
<p className="empty-zone">No items to show in this slot.</p> <p className="empty-zone">No items to show in this slot.</p>
+10 -2
View File
@@ -7,12 +7,15 @@ interface UpgradeCardProps {
upgrade: Upgrade; upgrade: Upgrade;
currentGold: number; currentGold: number;
currentEssence: number; currentEssence: number;
currentCrystals: number;
} }
const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps): React.JSX.Element => { const UpgradeCard = ({ upgrade, currentGold, currentEssence, currentCrystals }: UpgradeCardProps): React.JSX.Element => {
const { buyUpgrade } = useGame(); const { buyUpgrade } = useGame();
const canAfford = const canAfford =
currentGold >= upgrade.costGold && currentEssence >= upgrade.costEssence; currentGold >= upgrade.costGold &&
currentEssence >= upgrade.costEssence &&
currentCrystals >= (upgrade.costCrystals ?? 0);
if (!upgrade.unlocked) { if (!upgrade.unlocked) {
return ( return (
@@ -25,6 +28,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps)
<div className="upgrade-cost"> <div className="upgrade-cost">
{upgrade.costGold > 0 && <span>🪙 {upgrade.costGold.toLocaleString()}</span>} {upgrade.costGold > 0 && <span>🪙 {upgrade.costGold.toLocaleString()}</span>}
{upgrade.costEssence > 0 && <span> {upgrade.costEssence.toLocaleString()}</span>} {upgrade.costEssence > 0 && <span> {upgrade.costEssence.toLocaleString()}</span>}
{(upgrade.costCrystals ?? 0) > 0 && <span>💎 {upgrade.costCrystals?.toLocaleString()}</span>}
</div> </div>
<span className="upgrade-locked-label">Locked</span> <span className="upgrade-locked-label">Locked</span>
</div> </div>
@@ -50,6 +54,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps)
<div className="upgrade-cost"> <div className="upgrade-cost">
{upgrade.costGold > 0 && <span>🪙 {upgrade.costGold.toLocaleString()}</span>} {upgrade.costGold > 0 && <span>🪙 {upgrade.costGold.toLocaleString()}</span>}
{upgrade.costEssence > 0 && <span> {upgrade.costEssence.toLocaleString()}</span>} {upgrade.costEssence > 0 && <span> {upgrade.costEssence.toLocaleString()}</span>}
{(upgrade.costCrystals ?? 0) > 0 && <span>💎 {upgrade.costCrystals?.toLocaleString()}</span>}
</div> </div>
<button <button
className="buy-button" className="buy-button"
@@ -94,6 +99,7 @@ export const UpgradePanel = (): React.JSX.Element => {
upgrade={upgrade} upgrade={upgrade}
currentGold={state.resources.gold} currentGold={state.resources.gold}
currentEssence={state.resources.essence} currentEssence={state.resources.essence}
currentCrystals={state.resources.crystals}
/> />
))} ))}
{purchased.map((upgrade) => ( {purchased.map((upgrade) => (
@@ -102,6 +108,7 @@ export const UpgradePanel = (): React.JSX.Element => {
upgrade={upgrade} upgrade={upgrade}
currentGold={state.resources.gold} currentGold={state.resources.gold}
currentEssence={state.resources.essence} currentEssence={state.resources.essence}
currentCrystals={state.resources.crystals}
/> />
))} ))}
{showLocked && locked.map((upgrade) => ( {showLocked && locked.map((upgrade) => (
@@ -110,6 +117,7 @@ export const UpgradePanel = (): React.JSX.Element => {
upgrade={upgrade} upgrade={upgrade}
currentGold={state.resources.gold} currentGold={state.resources.gold}
currentEssence={state.resources.essence} currentEssence={state.resources.essence}
currentCrystals={state.resources.crystals}
/> />
))} ))}
</div> </div>
+56 -8
View File
@@ -25,6 +25,8 @@ interface GameContextValue {
buyAdventurer: (adventurerId: string) => void; buyAdventurer: (adventurerId: string) => void;
/** Buy an upgrade */ /** Buy an upgrade */
buyUpgrade: (upgradeId: string) => void; buyUpgrade: (upgradeId: string) => void;
/** Purchase a buyable equipment item */
buyEquipment: (equipmentId: string) => void;
/** Start a quest */ /** Start a quest */
startQuest: (questId: string) => void; startQuest: (questId: string) => void;
/** Challenge a boss — runs full server-side simulation */ /** Challenge a boss — runs full server-side simulation */
@@ -175,6 +177,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (!upgrade || !upgrade.unlocked || upgrade.purchased) return prev; if (!upgrade || !upgrade.unlocked || upgrade.purchased) return prev;
if (prev.resources.gold < upgrade.costGold) return prev; if (prev.resources.gold < upgrade.costGold) return prev;
if (prev.resources.essence < upgrade.costEssence) return prev; if (prev.resources.essence < upgrade.costEssence) return prev;
if (prev.resources.crystals < (upgrade.costCrystals ?? 0)) return prev;
return { return {
...prev, ...prev,
@@ -182,6 +185,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
...prev.resources, ...prev.resources,
gold: prev.resources.gold - upgrade.costGold, gold: prev.resources.gold - upgrade.costGold,
essence: prev.resources.essence - upgrade.costEssence, essence: prev.resources.essence - upgrade.costEssence,
crystals: prev.resources.crystals - (upgrade.costCrystals ?? 0),
}, },
upgrades: prev.upgrades.map((u) => upgrades: prev.upgrades.map((u) =>
u.id === upgradeId ? { ...u, purchased: true } : u, u.id === upgradeId ? { ...u, purchased: true } : u,
@@ -225,6 +229,37 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
}); });
}, []); }, []);
const buyEquipment = useCallback((equipmentId: string) => {
setState((prev) => {
if (!prev) return prev;
const item = (prev.equipment ?? []).find((e) => e.id === equipmentId);
if (!item || item.owned || !item.cost) return prev;
const { gold, essence, crystals } = item.cost;
if (prev.resources.gold < gold) return prev;
if (prev.resources.essence < essence) return prev;
if (prev.resources.crystals < crystals) return prev;
const slotAlreadyEquipped = (prev.equipment ?? []).some(
(e) => e.type === item.type && e.equipped,
);
return {
...prev,
resources: {
...prev.resources,
gold: prev.resources.gold - gold,
essence: prev.resources.essence - essence,
crystals: prev.resources.crystals - crystals,
},
equipment: (prev.equipment ?? []).map((e) => {
if (e.id === equipmentId) return { ...e, owned: true, equipped: !slotAlreadyEquipped };
return e;
}),
};
});
}, []);
const challengeBoss = useCallback(async (bossId: string) => { const challengeBoss = useCallback(async (bossId: string) => {
if (!stateRef.current) return; if (!stateRef.current) return;
const boss = stateRef.current.bosses.find((b) => b.id === bossId); const boss = stateRef.current.bosses.find((b) => b.id === bossId);
@@ -238,21 +273,33 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (!prev) return prev; if (!prev) return prev;
if (result.won) { if (result.won) {
const bossIndex = prev.bosses.findIndex((b) => b.id === bossId); const defeatedBoss = prev.bosses.find((b) => b.id === bossId);
const zoneBosses = prev.bosses.filter((b) => b.zoneId === defeatedBoss?.zoneId);
const zoneIdx = zoneBosses.findIndex((b) => b.id === bossId);
const nextZoneBossId = zoneBosses[zoneIdx + 1]?.id;
// Find newly unlocked zones and their first bosses
const newlyUnlockedZones = (prev.zones ?? []).filter((z) => z.unlockBossId === bossId && z.status === "locked");
const newZoneFirstBossIds = newlyUnlockedZones.map((z) => {
const firstBoss = prev.bosses.find((b) => b.zoneId === z.id);
return firstBoss?.id;
}).filter(Boolean);
return { return {
...prev, ...prev,
bosses: prev.bosses.map((b, idx) => { bosses: prev.bosses.map((b) => {
if (b.id === bossId) { if (b.id === bossId) return { ...b, status: "defeated" as const, currentHp: 0 };
return { ...b, status: "defeated" as const, currentHp: 0 }; if (b.id === nextZoneBossId && b.prestigeRequirement <= prev.prestige.count) {
return { ...b, status: "available" as const };
} }
if ( if (newZoneFirstBossIds.includes(b.id) && b.prestigeRequirement <= prev.prestige.count) {
idx === bossIndex + 1 &&
b.prestigeRequirement <= prev.prestige.count
) {
return { ...b, status: "available" as const }; return { ...b, status: "available" as const };
} }
return b; return b;
}), }),
zones: (prev.zones ?? []).map((z) =>
z.unlockBossId === bossId ? { ...z, status: "unlocked" as const } : z,
),
resources: result.rewards resources: result.rewards
? { ? {
...prev.resources, ...prev.resources,
@@ -332,6 +379,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
handleClick, handleClick,
buyAdventurer, buyAdventurer,
buyUpgrade, buyUpgrade,
buyEquipment,
startQuest, startQuest,
challengeBoss, challengeBoss,
equipItem, equipItem,
+6
View File
@@ -937,6 +937,12 @@ body {
font-size: 0.8rem; font-size: 0.8rem;
} }
.equipment-cost {
color: var(--colour-essence);
font-size: 0.8rem;
margin-top: 0.2rem;
}
.equipment-equipped-badge { .equipment-equipped-badge {
color: var(--colour-success); color: var(--colour-success);
font-size: 0.85rem; font-size: 0.85rem;
@@ -22,4 +22,6 @@ export interface Equipment {
owned: boolean; owned: boolean;
/** Whether this item is currently equipped (only one per type can be equipped) */ /** Whether this item is currently equipped (only one per type can be equipped) */
equipped: boolean; equipped: boolean;
/** If set, this item can be purchased directly rather than obtained via boss drops */
cost?: { gold: number; essence: number; crystals: number };
} }
+1
View File
@@ -16,6 +16,7 @@ export interface Upgrade {
multiplier: number; multiplier: number;
costGold: number; costGold: number;
costEssence: number; costEssence: number;
costCrystals: number;
purchased: boolean; purchased: boolean;
unlocked: boolean; unlocked: boolean;
} }