feat: v1 prototype — core game systems #30

Merged
naomi merged 84 commits from feat/prototype into main 2026-03-08 15:53:39 -07:00
23 changed files with 2288 additions and 91 deletions
Showing only changes of commit 5b4661b398 - Show all commits
+49
View File
@@ -0,0 +1,49 @@
# Elysium — Content Ideas
A running list of planned features and content additions. Strike through items as they're completed!
---
## 🌟 New Systems
- [ ] **Offline earnings** — When returning to the game, earn a percentage of what you'd have earned offline (cap at ~812 hours). Upgradeable via the prestige shop to increase the % and the time cap. Essential for an idle game!
- [ ] **Second prestige layer (Transcendence)** — Unlocked after ~10 prestiges. Sacrifice all runestones for a new currency ("Echoes"?). Echoes are permanent account-wide currency that persist across prestiges. Has its own upgrade tree with truly game-changing bonuses. Gives endgame players a long-term goal.
- [ ] **Daily challenges** — Three rotating objectives each day (e.g. kill X boss, earn X gold this run, complete X quests). Reward bonus runestones. Encourages daily logins even when idling comfortably.
- [ ] **Boss first-kill bounties** — Defeating a boss for the very first time grants a one-time runestone bonus. Rewards exploration and makes conquering a new zone feel extra satisfying.
- [ ] **Auto-prestige toggle** — Unlockable via the prestige shop. Automatically prestiges the moment the threshold is reached. Late-game convenience that dedicated players will love.
---
## 📦 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.
- [ ] **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.
- [ ] **Milestone prestige bonuses** — Every 5th prestige, earn a free prestige upgrade or a large runestone windfall. Gives players mini-goals within the prestige loop.
---
## 📊 UI / Statistics
- [ ] **Statistics panel** — All-time totals: gold earned across all runs, total prestiges, bosses defeated, quests completed, time played. Idle game players love seeing big numbers about their big numbers.
- [ ] **Last cloud save date + Force cloud save button** — Display when the last cloud save occurred (always visible, e.g. in the ResourceBar). Include a manual "Force Save" button for peace of mind.
---
## 💜 Priority Order (Suggested)
1. Offline earnings (core idle game feature)
2. Statistics panel (low effort, high satisfaction)
3. Daily challenges (retention driver)
4. Boss first-kill bounties (easy content win)
5. Milestone prestige bonuses (easy content win)
6. Equipment set bonuses (medium effort)
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)
+121
View File
@@ -232,4 +232,125 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [
count: 0,
unlocked: false,
},
{
id: "aether_weaver",
name: "Aether Weaver",
class: "mage",
level: 22,
goldPerSecond: 800_000_000,
essencePerSecond: 220_000,
combatPower: 2_700_000_000,
count: 0,
unlocked: false,
},
{
id: "titan_warrior",
name: "Titan Warrior",
class: "warrior",
level: 23,
goldPerSecond: 2_500_000_000,
essencePerSecond: 600_000,
combatPower: 8_000_000_000,
count: 0,
unlocked: false,
},
{
id: "nexus_sage",
name: "Nexus Sage",
class: "mage",
level: 24,
goldPerSecond: 7_500_000_000,
essencePerSecond: 1_600_000,
combatPower: 24_000_000_000,
count: 0,
unlocked: false,
},
{
id: "cosmos_knight",
name: "Cosmos Knight",
class: "paladin",
level: 25,
goldPerSecond: 22_000_000_000,
essencePerSecond: 4_500_000,
combatPower: 72_000_000_000,
count: 0,
unlocked: false,
},
{
id: "astral_sovereign",
name: "Astral Sovereign",
class: "warrior",
level: 26,
goldPerSecond: 65_000_000_000,
essencePerSecond: 12_000_000,
combatPower: 200_000_000_000,
count: 0,
unlocked: false,
},
{
id: "primordial_mage",
name: "Primordial Mage",
class: "mage",
level: 27,
goldPerSecond: 200_000_000_000,
essencePerSecond: 35_000_000,
combatPower: 600_000_000_000,
count: 0,
unlocked: false,
},
{
id: "reality_warden",
name: "Reality Warden",
class: "paladin",
level: 28,
goldPerSecond: 600_000_000_000,
essencePerSecond: 100_000_000,
combatPower: 1_800_000_000_000,
count: 0,
unlocked: false,
},
{
id: "infinity_ranger",
name: "Infinity Ranger",
class: "ranger",
level: 29,
goldPerSecond: 1_800_000_000_000,
essencePerSecond: 300_000_000,
combatPower: 5_500_000_000_000,
count: 0,
unlocked: false,
},
{
id: "oblivion_paladin",
name: "Oblivion Paladin",
class: "paladin",
level: 30,
goldPerSecond: 5_500_000_000_000,
essencePerSecond: 850_000_000,
combatPower: 16_000_000_000_000,
count: 0,
unlocked: false,
},
{
id: "transcendent_rogue",
name: "Transcendent Rogue",
class: "rogue",
level: 31,
goldPerSecond: 16_000_000_000_000,
essencePerSecond: 2_500_000_000,
combatPower: 50_000_000_000_000,
count: 0,
unlocked: false,
},
{
id: "omniversal_champion",
name: "Omniversal Champion",
class: "warrior",
level: 32,
goldPerSecond: 50_000_000_000_000,
essencePerSecond: 7_000_000_000,
combatPower: 150_000_000_000_000,
count: 0,
unlocked: false,
},
];
+414
View File
@@ -829,4 +829,418 @@ export const DEFAULT_BOSSES: Boss[] = [
prestigeRequirement: 25,
zoneId: "eternal_throne",
},
// ── Primordial Chaos ──────────────────────────────────────────────────────
{
id: "chaos_wyrm",
name: "The Chaos Wyrm",
description:
"A serpent of pure unformed potential, writhing through pre-creation. Every movement reshapes the chaos around it. Its scales are made of possibilities that never resolved.",
status: "locked",
maxHp: 1e26,
currentHp: 1e26,
damagePerSecond: 2e20,
goldReward: 1e27,
essenceReward: 1e23,
crystalReward: 2e20,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 26,
zoneId: "primordial_chaos",
},
{
id: "creation_engine",
name: "The Creation Engine",
description:
"Not alive — a mechanism of the chaos, producing and destroying matter in endless cycles. It has no awareness of your guild. That makes it no less lethal.",
status: "locked",
maxHp: 5e27,
currentHp: 5e27,
damagePerSecond: 8e21,
goldReward: 5e28,
essenceReward: 5e24,
crystalReward: 8e21,
upgradeRewards: ["aether_weaver_1"],
equipmentRewards: [],
prestigeRequirement: 27,
zoneId: "primordial_chaos",
},
{
id: "entropy_avatar",
name: "The Entropy Avatar",
description:
"A fragment of the force that will eventually end everything — visiting the chaos early, as it always does, to watch things fall apart. Your guild is an interesting disruption to its observations.",
status: "locked",
maxHp: 2e29,
currentHp: 2e29,
damagePerSecond: 4e23,
goldReward: 2e30,
essenceReward: 2e26,
crystalReward: 4e23,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 29,
zoneId: "primordial_chaos",
},
{
id: "primordial_titan",
name: "The Primordial Titan",
description:
"The first and largest thing to coalesce from the chaos — a being of pure unordered power that predates every law of physics your guild has ever relied upon. Defeating it will require those laws to hold long enough.",
status: "locked",
maxHp: 8e30,
currentHp: 8e30,
damagePerSecond: 2e25,
goldReward: 8e31,
essenceReward: 8e27,
crystalReward: 2e25,
upgradeRewards: [],
equipmentRewards: ["chaos_mantle", "titan_core"],
prestigeRequirement: 31,
zoneId: "primordial_chaos",
},
// ── Infinite Expanse ──────────────────────────────────────────────────────
{
id: "expanse_drifter",
name: "The Expanse Drifter",
description:
"Something vast that has been travelling the Infinite Expanse for so long that it has forgotten what it was looking for. Your guild is the first thing it has encountered that was worth stopping for.",
status: "locked",
maxHp: 3e33,
currentHp: 3e33,
damagePerSecond: 8e27,
goldReward: 3e34,
essenceReward: 3e30,
crystalReward: 8e27,
upgradeRewards: ["titan_warrior_1"],
equipmentRewards: [],
prestigeRequirement: 33,
zoneId: "infinite_expanse",
},
{
id: "horizon_beast",
name: "The Horizon Beast",
description:
"A creature as wide as the observable universe — which, in the Expanse, is not a helpful measurement. It is simply everywhere the horizon is, which in this place is everywhere.",
status: "locked",
maxHp: 1e37,
currentHp: 1e37,
damagePerSecond: 3e31,
goldReward: 1e38,
essenceReward: 1e34,
crystalReward: 3e31,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 35,
zoneId: "infinite_expanse",
},
{
id: "infinity_construct",
name: "The Infinity Construct",
description:
"A self-replicating intelligence that has filled the Expanse with copies of itself. Every copy has the same purpose: to be the last thing in the Expanse. Your guild will need to convince all of them otherwise.",
status: "locked",
maxHp: 5e40,
currentHp: 5e40,
damagePerSecond: 1e35,
goldReward: 5e41,
essenceReward: 5e37,
crystalReward: 1e35,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 37,
zoneId: "infinite_expanse",
},
{
id: "expanse_sovereign",
name: "The Expanse Sovereign",
description:
"The thing that claims the Infinite Expanse as its territory — which, given the name of the place, is an ambitious claim. It enforces this claim with power that has had infinite space to accumulate.",
status: "locked",
maxHp: 2e44,
currentHp: 2e44,
damagePerSecond: 5e38,
goldReward: 2e45,
essenceReward: 2e41,
crystalReward: 5e38,
upgradeRewards: [],
equipmentRewards: ["expanse_blade", "void_armour_mk2"],
prestigeRequirement: 39,
zoneId: "infinite_expanse",
},
// ── Reality Forge ─────────────────────────────────────────────────────────
{
id: "forge_guardian",
name: "The Forge Guardian",
description:
"A creation of the Forge itself — something that was made to protect the making of things. It has never had to do this before. It finds it straightforward.",
status: "locked",
maxHp: 8e47,
currentHp: 8e47,
damagePerSecond: 2e42,
goldReward: 8e48,
essenceReward: 8e44,
crystalReward: 2e42,
upgradeRewards: ["nexus_sage_1"],
equipmentRewards: [],
prestigeRequirement: 41,
zoneId: "reality_forge",
},
{
id: "reality_shaper",
name: "The Reality Shaper",
description:
"One of the workers of the Forge — a being whose purpose is to take raw existence and hammer it into something coherent. It does not appreciate your guild's interruption of its work.",
status: "locked",
maxHp: 4e52,
currentHp: 4e52,
damagePerSecond: 1e47,
goldReward: 4e53,
essenceReward: 4e49,
crystalReward: 1e47,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 44,
zoneId: "reality_forge",
},
{
id: "creation_prime",
name: "The Creation Prime",
description:
"The first worker, the original builder — the thing that shaped the template every universe since has been based on. It has been refining the template since before time. Your guild is not part of the template.",
status: "locked",
maxHp: 2e57,
currentHp: 2e57,
damagePerSecond: 6e51,
goldReward: 2e58,
essenceReward: 2e54,
crystalReward: 6e51,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 47,
zoneId: "reality_forge",
},
{
id: "reality_architect",
name: "The Reality Architect",
description:
"The designer of all that exists — the being who decided what the rules would be. Every law of physics is its handwriting. Defeating it will not change the laws, but it will change the architect.",
status: "locked",
maxHp: 8e61,
currentHp: 8e61,
damagePerSecond: 2e56,
goldReward: 8e62,
essenceReward: 8e58,
crystalReward: 2e56,
upgradeRewards: [],
equipmentRewards: ["cosmos_blade", "reality_plate"],
prestigeRequirement: 49,
zoneId: "reality_forge",
},
// ── Cosmic Maelstrom ──────────────────────────────────────────────────────
{
id: "storm_colossus",
name: "The Storm Colossus",
description:
"A being born from the intersection of all cosmic forces — not created, simply precipitated out of the violence as inevitably as lightning from a storm cloud. It has been raging since the universe learned what force was.",
status: "locked",
maxHp: 4e65,
currentHp: 4e65,
damagePerSecond: 1e60,
goldReward: 4e66,
essenceReward: 4e62,
crystalReward: 1e60,
upgradeRewards: ["cosmos_knight_1"],
equipmentRewards: [],
prestigeRequirement: 51,
zoneId: "cosmic_maelstrom",
},
{
id: "force_prime",
name: "The Force Prime",
description:
"The ur-force from which all other forces derived their nature. Gravity, electromagnetism, the nuclear forces — all are pale echoes of what this being embodies. Your guild will feel all of them at once.",
status: "locked",
maxHp: 2e71,
currentHp: 2e71,
damagePerSecond: 6e65,
goldReward: 2e72,
essenceReward: 2e68,
crystalReward: 6e65,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 54,
zoneId: "cosmic_maelstrom",
},
{
id: "maelstrom_god",
name: "The Maelstrom God",
description:
"The deity of devastation — the divine principle that ensures the universe never becomes too comfortable. It was responsible for every catastrophe that has ever reshaped a world. Your guild is its latest project.",
status: "locked",
maxHp: 1e77,
currentHp: 1e77,
damagePerSecond: 3e71,
goldReward: 1e78,
essenceReward: 1e74,
crystalReward: 3e71,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 57,
zoneId: "cosmic_maelstrom",
},
{
id: "cosmic_annihilator",
name: "The Cosmic Annihilator",
description:
"The counterpart to the Reality Architect — not a destroyer but a pruner, removing the universes that failed to meet the Architect's standards. It is very good at its job, and your universe has been on its list for some time.",
status: "locked",
maxHp: 5e82,
currentHp: 5e82,
damagePerSecond: 1e77,
goldReward: 5e83,
essenceReward: 5e79,
crystalReward: 1e77,
upgradeRewards: [],
equipmentRewards: ["maelstrom_edge", "cosmic_plate"],
prestigeRequirement: 59,
zoneId: "cosmic_maelstrom",
},
// ── Primeval Sanctum ──────────────────────────────────────────────────────
{
id: "ancient_sentinel",
name: "The Ancient Sentinel",
description:
"A guardian placed here before memory — before the concept of guarding existed, placed by something that knew guardians would eventually be needed. It has been waiting with perfect patience.",
status: "locked",
maxHp: 2e88,
currentHp: 2e88,
damagePerSecond: 5e82,
goldReward: 2e89,
essenceReward: 2e85,
crystalReward: 5e82,
upgradeRewards: ["astral_sovereign_1"],
equipmentRewards: [],
prestigeRequirement: 61,
zoneId: "primeval_sanctum",
},
{
id: "time_elder",
name: "The Time Elder",
description:
"The oldest living thing — living by a definition so broad it encompasses states your guild cannot recognise as life. It has observed every moment from the beginning and finds your guild mildly interesting by comparison.",
status: "locked",
maxHp: 1e95,
currentHp: 1e95,
damagePerSecond: 3e89,
goldReward: 1e96,
essenceReward: 1e92,
crystalReward: 3e89,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 65,
zoneId: "primeval_sanctum",
},
{
id: "origin_beast",
name: "The Origin Beast",
description:
"The creature that was present at the first moment — not because it was created then, but because it was always there, before the universe caught up to it. It has been here since before here existed.",
status: "locked",
maxHp: 8e101,
currentHp: 8e101,
damagePerSecond: 2e96,
goldReward: 8e102,
essenceReward: 8e98,
crystalReward: 2e96,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 69,
zoneId: "primeval_sanctum",
},
{
id: "primeval_god",
name: "The Primeval God",
description:
"Not a god that was worshipped — a god that simply is, regardless of worship. It does not require belief to exist. It exists prior to the ability to believe or disbelieve in anything.",
status: "locked",
maxHp: 5e108,
currentHp: 5e108,
damagePerSecond: 1e103,
goldReward: 5e109,
essenceReward: 5e105,
crystalReward: 1e103,
upgradeRewards: [],
equipmentRewards: ["primeval_blade", "ancient_aegis"],
prestigeRequirement: 74,
zoneId: "primeval_sanctum",
},
// ── The Absolute ──────────────────────────────────────────────────────────
{
id: "absolute_herald",
name: "The Absolute Herald",
description:
"The announcement of finality — not a creature but the moment before the last moment, given agency. It is here to tell your guild that this is where everything ends. Your guild declines to accept the announcement.",
status: "locked",
maxHp: 2e116,
currentHp: 2e116,
damagePerSecond: 5e110,
goldReward: 2e117,
essenceReward: 2e113,
crystalReward: 5e110,
upgradeRewards: ["primordial_mage_1"],
equipmentRewards: [],
prestigeRequirement: 76,
zoneId: "the_absolute",
},
{
id: "void_convergence",
name: "The Void Convergence",
description:
"Every void, every absence, every nothing that has ever existed converging into a single point. The gravitational pull of absolute nothingness. Your guild must push against the pull of all that is not.",
status: "locked",
maxHp: 1e125,
currentHp: 1e125,
damagePerSecond: 3e119,
goldReward: 1e126,
essenceReward: 1e122,
crystalReward: 3e119,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 79,
zoneId: "the_absolute",
},
{
id: "eternal_end",
name: "The Eternal End",
description:
"The last thing that will ever exist — visiting now, ahead of schedule, drawn by the power your guild has accumulated. It does not consider this inconvenient. Everything ends eventually. It is simply efficient.",
status: "locked",
maxHp: 5e134,
currentHp: 5e134,
damagePerSecond: 1e129,
goldReward: 5e135,
essenceReward: 5e131,
crystalReward: 1e129,
upgradeRewards: [],
equipmentRewards: [],
prestigeRequirement: 83,
zoneId: "the_absolute",
},
{
id: "the_absolute_one",
name: "The Absolute One",
description:
"Beyond description. Beyond category. The terminal point of all power, all existence, all possibility. There is nothing after this. Your guild has come to this nothing and refused it. That, in itself, is the greatest achievement in the history of anything.",
status: "locked",
maxHp: 2e145,
currentHp: 2e145,
damagePerSecond: 5e139,
goldReward: 2e146,
essenceReward: 2e142,
crystalReward: 5e139,
upgradeRewards: [],
equipmentRewards: ["absolute_blade", "eternity_plate", "omniversal_core"],
prestigeRequirement: 90,
zoneId: "the_absolute",
},
];
+206
View File
@@ -0,0 +1,206 @@
import type { PrestigeUpgrade } from "@elysium/types";
export const DEFAULT_PRESTIGE_UPGRADES: PrestigeUpgrade[] = [
// ── Global Income Tiers ───────────────────────────────────────────────────
{
id: "income_1",
name: "Runestone Blessing I",
description: "The first runestone awakens dormant power in your guild. All production ×1.25.",
category: "income",
runestonesCost: 10,
multiplier: 1.25,
},
{
id: "income_2",
name: "Runestone Blessing II",
description: "Deeper runestone resonance amplifies your workforce. All production ×1.5.",
category: "income",
runestonesCost: 25,
multiplier: 1.5,
},
{
id: "income_3",
name: "Runestone Blessing III",
description: "The runes sing with accumulated wisdom. All production ×2.",
category: "income",
runestonesCost: 60,
multiplier: 2,
},
{
id: "income_4",
name: "Runic Surge I",
description: "Runestone energy surges through your guild's operations. All production ×3.",
category: "income",
runestonesCost: 150,
multiplier: 3,
},
{
id: "income_5",
name: "Runic Surge II",
description: "The surge intensifies, pushing limits thought impossible. All production ×5.",
category: "income",
runestonesCost: 350,
multiplier: 5,
},
{
id: "income_6",
name: "Runic Surge III",
description: "An overwhelming tide of runic energy floods your operations. All production ×10.",
category: "income",
runestonesCost: 800,
multiplier: 10,
},
{
id: "income_7",
name: "Ancient Inscription I",
description:
"You decipher ancient runic inscriptions that unlock vast potential. All production ×25.",
category: "income",
runestonesCost: 2_000,
multiplier: 25,
},
{
id: "income_8",
name: "Ancient Inscription II",
description: "Deeper inscriptions reveal secrets of primordial power. All production ×50.",
category: "income",
runestonesCost: 5_000,
multiplier: 50,
},
{
id: "income_9",
name: "Ancient Inscription III",
description: "The full inscription blazes with world-shaping power. All production ×100.",
category: "income",
runestonesCost: 12_000,
multiplier: 100,
},
{
id: "income_10",
name: "Eternal Rune I",
description:
"The oldest runes, carved before memory began, yield their secrets at last. All production ×500.",
category: "income",
runestonesCost: 30_000,
multiplier: 500,
},
{
id: "income_11",
name: "Eternal Rune II",
description:
"Eternal runes resonate with the heartbeat of creation itself. All production ×1,000.",
category: "income",
runestonesCost: 80_000,
multiplier: 1_000,
},
// ── Click Power ───────────────────────────────────────────────────────────
{
id: "click_power_1",
name: "Runic Strike I",
description: "Infuse your personal strikes with runestone energy. Click power ×2.",
category: "click",
runestonesCost: 15,
multiplier: 2,
},
{
id: "click_power_2",
name: "Runic Strike II",
description: "Your strikes crackle with compounded runic force. Click power ×5.",
category: "click",
runestonesCost: 75,
multiplier: 5,
},
{
id: "click_power_3",
name: "Runic Strike III",
description: "Every click channels the weight of all your past lives. Click power ×20.",
category: "click",
runestonesCost: 400,
multiplier: 20,
},
{
id: "click_power_4",
name: "World-Breaker Click",
description: "A single click now carries the force of a falling empire. Click power ×100.",
category: "click",
runestonesCost: 2_500,
multiplier: 100,
},
// ── Essence Production ────────────────────────────────────────────────────
{
id: "essence_1",
name: "Essence Attunement I",
description: "Runestone resonance amplifies your essence gathering. Essence production ×2.",
category: "essence",
runestonesCost: 20,
multiplier: 2,
},
{
id: "essence_2",
name: "Essence Attunement II",
description: "Deep attunement draws essence from previously invisible sources. Essence production ×5.",
category: "essence",
runestonesCost: 120,
multiplier: 5,
},
{
id: "essence_3",
name: "Essence Attunement III",
description: "Your guild breathes essence as naturally as air. Essence production ×20.",
category: "essence",
runestonesCost: 700,
multiplier: 20,
},
{
id: "essence_4",
name: "Essence Attunement IV",
description: "Essence flows in torrents from every corner of every world. Essence production ×100.",
category: "essence",
runestonesCost: 4_000,
multiplier: 100,
},
// ── Crystal Production ────────────────────────────────────────────────────
{
id: "crystal_1",
name: "Crystal Resonance I",
description: "Runestones vibrate in harmony with crystal structures. Crystal rewards ×2.",
category: "crystals",
runestonesCost: 30,
multiplier: 2,
},
{
id: "crystal_2",
name: "Crystal Resonance II",
description: "The resonance deepens, shattering crystal barriers. Crystal rewards ×5.",
category: "crystals",
runestonesCost: 200,
multiplier: 5,
},
{
id: "crystal_3",
name: "Crystal Resonance III",
description: "Pure resonance crystallises reality into abundance. Crystal rewards ×25.",
category: "crystals",
runestonesCost: 1_200,
multiplier: 25,
},
// ── Runestone Meta-Upgrade ────────────────────────────────────────────────
{
id: "runestone_gain_1",
name: "Runic Legacy",
description:
"Your runestone attunement grows with each prestige. Earn 25% more runestones from future prestiges.",
category: "runestones",
runestonesCost: 50,
multiplier: 1.25,
},
{
id: "runestone_gain_2",
name: "Eternal Legacy",
description:
"Your legend transcends individual lifetimes. Earn 50% more runestones from future prestiges.",
category: "runestones",
runestonesCost: 500,
multiplier: 1.5,
},
];
+579
View File
@@ -42,6 +42,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["goblin_camp"],
zoneId: "verdant_vale",
combatPowerRequired: 10,
},
{
id: "ancient_ruins",
@@ -56,6 +57,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["haunted_mine"],
zoneId: "verdant_vale",
combatPowerRequired: 50,
},
// ── Shattered Ruins ───────────────────────────────────────────────────────
{
@@ -73,6 +75,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: [],
zoneId: "shattered_ruins",
combatPowerRequired: 500,
},
{
id: "crumbling_fortress",
@@ -89,6 +92,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["necromancer_tower"],
zoneId: "shattered_ruins",
combatPowerRequired: 2_000,
},
{
id: "cursed_library",
@@ -105,6 +109,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["crumbling_fortress"],
zoneId: "shattered_ruins",
combatPowerRequired: 8_000,
},
{
id: "dragon_lair",
@@ -121,6 +126,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["cursed_library"],
zoneId: "shattered_ruins",
combatPowerRequired: 30_000,
},
// ── Shadow Marshes ────────────────────────────────────────────────────────
{
@@ -136,6 +142,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: [],
zoneId: "shadow_marshes",
combatPowerRequired: 5_000,
},
{
id: "witch_coven",
@@ -150,6 +157,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["shadow_mere"],
zoneId: "shadow_marshes",
combatPowerRequired: 20_000,
},
{
id: "sunken_temple",
@@ -165,6 +173,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["witch_coven"],
zoneId: "shadow_marshes",
combatPowerRequired: 80_000,
},
{
id: "plague_ruins",
@@ -180,6 +189,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["sunken_temple"],
zoneId: "shadow_marshes",
combatPowerRequired: 300_000,
},
// ── Frozen Peaks ──────────────────────────────────────────────────────────
{
@@ -196,6 +206,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: [],
zoneId: "frozen_peaks",
combatPowerRequired: 100_000,
},
{
id: "ice_caves",
@@ -211,6 +222,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["frozen_wastes"],
zoneId: "frozen_peaks",
combatPowerRequired: 400_000,
},
{
id: "storm_citadel",
@@ -226,6 +238,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["ice_caves"],
zoneId: "frozen_peaks",
combatPowerRequired: 1_500_000,
},
// ── Volcanic Depths ───────────────────────────────────────────────────────
{
@@ -241,6 +254,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: [],
zoneId: "volcanic_depths",
combatPowerRequired: 2_000_000,
},
{
id: "fire_temple",
@@ -256,6 +270,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["lava_flows"],
zoneId: "volcanic_depths",
combatPowerRequired: 8_000_000,
},
{
id: "magma_caverns",
@@ -271,6 +286,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["fire_temple"],
zoneId: "volcanic_depths",
combatPowerRequired: 30_000_000,
},
{
id: "the_forge",
@@ -286,6 +302,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["magma_caverns"],
zoneId: "volcanic_depths",
combatPowerRequired: 120_000_000,
},
// ── Astral Void ───────────────────────────────────────────────────────────
{
@@ -301,6 +318,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: [],
zoneId: "astral_void",
combatPowerRequired: 50_000_000,
},
{
id: "star_graveyard",
@@ -316,6 +334,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["void_rift"],
zoneId: "astral_void",
combatPowerRequired: 200_000_000,
},
{
id: "between_worlds",
@@ -331,6 +350,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["star_graveyard"],
zoneId: "astral_void",
combatPowerRequired: 800_000_000,
},
{
id: "the_end",
@@ -346,6 +366,7 @@ export const DEFAULT_QUESTS: Quest[] = [
],
prerequisiteIds: ["between_worlds"],
zoneId: "astral_void",
combatPowerRequired: 3_000_000_000,
},
// ── Celestial Reaches ─────────────────────────────────────────────────────
{
@@ -898,4 +919,562 @@ export const DEFAULT_QUESTS: Quest[] = [
prerequisiteIds: ["the_final_ascent"],
zoneId: "eternal_throne",
},
// ── Primordial Chaos ──────────────────────────────────────────────────────
{
id: "chaos_entry",
name: "Into the Chaos",
description:
"Your guild steps beyond the throne into something that has no rules — a place where the very concept of place is contested. Every step forward is an act of defiance against the universe's first draft of itself.",
status: "locked",
durationSeconds: 10 * 60 * 60,
rewards: [
{ type: "gold", amount: 8e27 },
{ type: "essence", amount: 3e24 },
{ type: "adventurer", targetId: "aether_weaver" },
],
prerequisiteIds: [],
zoneId: "primordial_chaos",
},
{
id: "chaos_currents",
name: "The Chaos Currents",
description:
"Rivers of raw creation flow through the primordial chaos — not water but pure potential, capable of transforming anything they touch into anything else entirely.",
status: "locked",
durationSeconds: 18 * 60 * 60,
rewards: [
{ type: "gold", amount: 4e28 },
{ type: "essence", amount: 1.5e25 },
{ type: "crystals", amount: 5e21 },
],
prerequisiteIds: ["chaos_entry"],
zoneId: "primordial_chaos",
},
{
id: "unformed_wastes",
name: "The Unformed Wastes",
description:
"A region of the chaos where the argument between existence and non-existence has not yet produced a winner — where matter and anti-matter coexist in violent, constant negotiation.",
status: "locked",
durationSeconds: 30 * 60 * 60,
rewards: [
{ type: "gold", amount: 2e29 },
{ type: "essence", amount: 8e25 },
{ type: "crystals", amount: 2e22 },
{ type: "adventurer", targetId: "titan_warrior" },
],
prerequisiteIds: ["chaos_currents"],
zoneId: "primordial_chaos",
},
{
id: "potential_vaults",
name: "The Vaults of Potential",
description:
"Every possibility that has never occurred is stored here — in vaults that have no walls, containing things that have no form. Your guild navigates them by deciding what they want to find, and finding it.",
status: "locked",
durationSeconds: 45 * 60 * 60,
rewards: [
{ type: "gold", amount: 1e30 },
{ type: "essence", amount: 4e26 },
{ type: "crystals", amount: 8e22 },
],
prerequisiteIds: ["unformed_wastes"],
zoneId: "primordial_chaos",
},
{
id: "creation_cradle",
name: "The Creation Cradle",
description:
"The origin point of everything — not a place but the idea of the first place, preserved in the chaos as a monument to the moment reality decided to exist.",
status: "locked",
durationSeconds: 65 * 60 * 60,
rewards: [
{ type: "gold", amount: 6e30 },
{ type: "essence", amount: 2e27 },
{ type: "crystals", amount: 4e23 },
{ type: "upgrade", targetId: "titan_warrior_1" },
],
prerequisiteIds: ["potential_vaults"],
zoneId: "primordial_chaos",
},
{
id: "chaos_chronicle",
name: "The Chaos Chronicle",
description:
"The record of everything that almost was — every universe that the chaos produced and discarded before settling on this one. Your guild reads it and understands, for the first time, how unlikely they are.",
status: "locked",
durationSeconds: 90 * 60 * 60,
rewards: [
{ type: "gold", amount: 3e31 },
{ type: "essence", amount: 1e28 },
{ type: "crystals", amount: 2e24 },
],
prerequisiteIds: ["creation_cradle"],
zoneId: "primordial_chaos",
},
// ── Infinite Expanse ──────────────────────────────────────────────────────
{
id: "first_horizon",
name: "The First Horizon",
description:
"The edge of the knowable — not because nothing lies beyond, but because the Expanse has no edges and every horizon is also a centre. Your guild walks toward a destination that keeps receding at the exact speed they approach it.",
status: "locked",
durationSeconds: 12 * 60 * 60,
rewards: [
{ type: "gold", amount: 1e33 },
{ type: "essence", amount: 4e29 },
{ type: "adventurer", targetId: "nexus_sage" },
],
prerequisiteIds: [],
zoneId: "infinite_expanse",
},
{
id: "endless_sea",
name: "The Endless Sea",
description:
"An ocean with no shores, no depth, no surface — a body of liquid possibility that extends infinitely in all directions, including inward. Your guild sails it without a ship and arrives exactly when they decide to.",
status: "locked",
durationSeconds: 22 * 60 * 60,
rewards: [
{ type: "gold", amount: 6e34 },
{ type: "essence", amount: 2e31 },
{ type: "crystals", amount: 5e27 },
],
prerequisiteIds: ["first_horizon"],
zoneId: "infinite_expanse",
},
{
id: "expanse_ruins",
name: "The Expanse Ruins",
description:
"Civilisations that attempted the Expanse before your guild and ran out of universe. Their ruins drift without reference points, enormous and silent, a reminder that infinity has claimed predecessors.",
status: "locked",
durationSeconds: 36 * 60 * 60,
rewards: [
{ type: "gold", amount: 3e36 },
{ type: "essence", amount: 1e33 },
{ type: "crystals", amount: 2.5e29 },
{ type: "adventurer", targetId: "cosmos_knight" },
],
prerequisiteIds: ["endless_sea"],
zoneId: "infinite_expanse",
},
{
id: "infinite_archive",
name: "The Infinite Archive",
description:
"A library with no walls, cataloguing everything that exists across all of infinite space. The catalogue itself is infinite. The librarian is very tired.",
status: "locked",
durationSeconds: 55 * 60 * 60,
rewards: [
{ type: "gold", amount: 1.5e38 },
{ type: "essence", amount: 5e34 },
{ type: "crystals", amount: 1e31 },
{ type: "upgrade", targetId: "nexus_sage_1" },
],
prerequisiteIds: ["expanse_ruins"],
zoneId: "infinite_expanse",
},
{
id: "paradox_plains",
name: "The Paradox Plains",
description:
"A region where the Expanse loops back on itself — where every direction is simultaneously every other direction, and travel requires your guild to stop thinking about it too hard.",
status: "locked",
durationSeconds: 80 * 60 * 60,
rewards: [
{ type: "gold", amount: 8e39 },
{ type: "essence", amount: 2.5e36 },
{ type: "crystals", amount: 5e32 },
],
prerequisiteIds: ["infinite_archive"],
zoneId: "infinite_expanse",
},
{
id: "expanse_codex",
name: "The Expanse Codex",
description:
"The complete record of all infinite things — compressed, impossibly, into a document your guild can almost read. What they can read changes everything they thought they understood about the word 'everything'.",
status: "locked",
durationSeconds: 110 * 60 * 60,
rewards: [
{ type: "gold", amount: 4e41 },
{ type: "essence", amount: 1.2e38 },
{ type: "crystals", amount: 2.5e34 },
],
prerequisiteIds: ["paradox_plains"],
zoneId: "infinite_expanse",
},
// ── Reality Forge ─────────────────────────────────────────────────────────
{
id: "forge_entrance",
name: "The Forge Entrance",
description:
"The door to the Reality Forge has been open since the moment reality started — left ajar because the workers never thought anyone else would find it. Your guild finds it.",
status: "locked",
durationSeconds: 14 * 60 * 60,
rewards: [
{ type: "gold", amount: 2e44 },
{ type: "essence", amount: 6e40 },
{ type: "adventurer", targetId: "astral_sovereign" },
],
prerequisiteIds: [],
zoneId: "reality_forge",
},
{
id: "blueprint_vault",
name: "The Blueprint Vault",
description:
"The Forge keeps the blueprints for every universe it has ever built — and the rejected designs for the ones it hasn't. Some of those rejected blueprints are disturbingly appealing.",
status: "locked",
durationSeconds: 25 * 60 * 60,
rewards: [
{ type: "gold", amount: 1e46 },
{ type: "essence", amount: 3e42 },
{ type: "crystals", amount: 2e38 },
],
prerequisiteIds: ["forge_entrance"],
zoneId: "reality_forge",
},
{
id: "creation_workshop",
name: "The Creation Workshop",
description:
"The active floor of the Forge — where new realities are being assembled right now, and your guild must navigate between workbenches containing half-finished universes without knocking anything over.",
status: "locked",
durationSeconds: 40 * 60 * 60,
rewards: [
{ type: "gold", amount: 5e47 },
{ type: "essence", amount: 1.5e44 },
{ type: "crystals", amount: 1e40 },
{ type: "adventurer", targetId: "primordial_mage" },
],
prerequisiteIds: ["blueprint_vault"],
zoneId: "reality_forge",
},
{
id: "laws_engine",
name: "The Laws Engine",
description:
"The mechanism that produces the laws of physics — an engine running since the first moment, churning out constants and rules that every universe obeys without knowing why. Your guild sees the source code.",
status: "locked",
durationSeconds: 60 * 60 * 60,
rewards: [
{ type: "gold", amount: 2.5e49 },
{ type: "essence", amount: 8e45 },
{ type: "crystals", amount: 5e41 },
{ type: "upgrade", targetId: "cosmos_knight_1" },
],
prerequisiteIds: ["creation_workshop"],
zoneId: "reality_forge",
},
{
id: "forge_heart",
name: "The Forge Heart",
description:
"The power source of the Reality Forge — not a furnace but a contained singularity, burning with the same energy that ignited the first universe. Your guild siphons from it. The Forge barely notices.",
status: "locked",
durationSeconds: 85 * 60 * 60,
rewards: [
{ type: "gold", amount: 1.2e51 },
{ type: "essence", amount: 4e47 },
{ type: "crystals", amount: 2.5e43 },
],
prerequisiteIds: ["laws_engine"],
zoneId: "reality_forge",
},
{
id: "forge_chronicle",
name: "The Forge Chronicle",
description:
"The record of every reality the Forge has produced — every universe that exists or ever existed, with notes on what worked and what didn't. Your guild's universe has several notes. Most are surprising.",
status: "locked",
durationSeconds: 120 * 60 * 60,
rewards: [
{ type: "gold", amount: 6e52 },
{ type: "essence", amount: 2e49 },
{ type: "crystals", amount: 1.2e45 },
],
prerequisiteIds: ["forge_heart"],
zoneId: "reality_forge",
},
// ── Cosmic Maelstrom ──────────────────────────────────────────────────────
{
id: "maelstrom_entry",
name: "The Maelstrom's Edge",
description:
"The outermost reach of the Cosmic Maelstrom — where everything moves at a speed that makes stars look stationary. Your guild anchors itself in the relative calm of its periphery and begins to push inward.",
status: "locked",
durationSeconds: 16 * 60 * 60,
rewards: [
{ type: "gold", amount: 3e55 },
{ type: "essence", amount: 1e52 },
{ type: "adventurer", targetId: "reality_warden" },
],
prerequisiteIds: [],
zoneId: "cosmic_maelstrom",
},
{
id: "force_nexus",
name: "The Force Nexus",
description:
"The point where every cosmic force intersects — where gravity and electromagnetism and every other fundamental force meet and argue. The argument is conducted at energies that reshape matter.",
status: "locked",
durationSeconds: 28 * 60 * 60,
rewards: [
{ type: "gold", amount: 1.5e58 },
{ type: "essence", amount: 5e54 },
{ type: "crystals", amount: 3e50 },
],
prerequisiteIds: ["maelstrom_entry"],
zoneId: "cosmic_maelstrom",
},
{
id: "storm_cauldron",
name: "The Storm Cauldron",
description:
"A region where cosmic storms have been brewing since the beginning of time, compounding on themselves into intensities that no physical object should be able to survive. Your guild survives.",
status: "locked",
durationSeconds: 45 * 60 * 60,
rewards: [
{ type: "gold", amount: 8e60 },
{ type: "essence", amount: 2.5e57 },
{ type: "crystals", amount: 1.5e53 },
{ type: "adventurer", targetId: "infinity_ranger" },
],
prerequisiteIds: ["force_nexus"],
zoneId: "cosmic_maelstrom",
},
{
id: "annihilation_fields",
name: "The Annihilation Fields",
description:
"Regions of space where creation and destruction happen simultaneously at rates that would erase continents. Your guild navigates the moments between creation and erasure with precision that surprises even themselves.",
status: "locked",
durationSeconds: 65 * 60 * 60,
rewards: [
{ type: "gold", amount: 4e63 },
{ type: "essence", amount: 1.2e60 },
{ type: "crystals", amount: 7e55 },
{ type: "upgrade", targetId: "astral_sovereign_1" },
],
prerequisiteIds: ["storm_cauldron"],
zoneId: "cosmic_maelstrom",
},
{
id: "convergence_point",
name: "The Convergence Point",
description:
"The centre of the Cosmic Maelstrom — the point toward which every force converges and from which everything radiates. Being here is being at the exact centre of all physical law. It is very loud.",
status: "locked",
durationSeconds: 90 * 60 * 60,
rewards: [
{ type: "gold", amount: 2e66 },
{ type: "essence", amount: 6e62 },
{ type: "crystals", amount: 3.5e58 },
],
prerequisiteIds: ["annihilation_fields"],
zoneId: "cosmic_maelstrom",
},
{
id: "maelstrom_codex",
name: "The Maelstrom Codex",
description:
"The record kept in the eye of the storm — the one place calm enough to write, where every force is in perfect balance. Your guild adds their chapter in the moments before the balance shifts again.",
status: "locked",
durationSeconds: 130 * 60 * 60,
rewards: [
{ type: "gold", amount: 1e69 },
{ type: "essence", amount: 3e65 },
{ type: "crystals", amount: 1.8e61 },
],
prerequisiteIds: ["convergence_point"],
zoneId: "cosmic_maelstrom",
},
// ── Primeval Sanctum ──────────────────────────────────────────────────────
{
id: "sanctum_gate",
name: "The Sanctum Gate",
description:
"The entrance to the oldest place — a threshold that does not open because it was never closed. It merely requires you to be old enough, deep enough, powerful enough to perceive it.",
status: "locked",
durationSeconds: 18 * 60 * 60,
rewards: [
{ type: "gold", amount: 5e72 },
{ type: "essence", amount: 1.5e69 },
{ type: "adventurer", targetId: "oblivion_paladin" },
],
prerequisiteIds: [],
zoneId: "primeval_sanctum",
},
{
id: "memory_vaults",
name: "The Memory Vaults",
description:
"The sanctum stores every moment that has ever occurred — not as records but as living impressions, still occurring in perpetual replay. Your guild walks through history as it happens, over and over.",
status: "locked",
durationSeconds: 32 * 60 * 60,
rewards: [
{ type: "gold", amount: 2.5e76 },
{ type: "essence", amount: 7e72 },
{ type: "crystals", amount: 4e68 },
],
prerequisiteIds: ["sanctum_gate"],
zoneId: "primeval_sanctum",
},
{
id: "origin_halls",
name: "The Origin Halls",
description:
"The halls where everything began — not the physical beginning, but the idea of beginning itself, preserved here as the sanctum's most sacred artefact. To walk these halls is to understand why anything started.",
status: "locked",
durationSeconds: 50 * 60 * 60,
rewards: [
{ type: "gold", amount: 1.2e80 },
{ type: "essence", amount: 3.5e76 },
{ type: "crystals", amount: 2e72 },
{ type: "adventurer", targetId: "transcendent_rogue" },
],
prerequisiteIds: ["memory_vaults"],
zoneId: "primeval_sanctum",
},
{
id: "first_light_hall",
name: "The Hall of First Light",
description:
"The chamber where the first photon was produced — still illuminated by that original light, unchanged for all of time. The warmth here is the warmth of the universe's childhood.",
status: "locked",
durationSeconds: 72 * 60 * 60,
rewards: [
{ type: "gold", amount: 6e83 },
{ type: "essence", amount: 1.8e80 },
{ type: "crystals", amount: 1e76 },
{ type: "upgrade", targetId: "primordial_mage_1" },
],
prerequisiteIds: ["origin_halls"],
zoneId: "primeval_sanctum",
},
{
id: "before_time",
name: "Before Time",
description:
"A region of the sanctum that predates the concept of sequence — where cause does not reliably precede effect, and your guild must navigate by intention rather than direction.",
status: "locked",
durationSeconds: 100 * 60 * 60,
rewards: [
{ type: "gold", amount: 3e87 },
{ type: "essence", amount: 9e83 },
{ type: "crystals", amount: 5e79 },
],
prerequisiteIds: ["first_light_hall"],
zoneId: "primeval_sanctum",
},
{
id: "sanctum_chronicle",
name: "The Sanctum Chronicle",
description:
"The complete record of all primeval things — every first moment of every concept that has ever existed, bound together in something that predates writing, reading, and the idea of records. Your guild understands it anyway.",
status: "locked",
durationSeconds: 144 * 60 * 60,
rewards: [
{ type: "gold", amount: 1.5e91 },
{ type: "essence", amount: 4.5e87 },
{ type: "crystals", amount: 2.5e83 },
],
prerequisiteIds: ["before_time"],
zoneId: "primeval_sanctum",
},
// ── The Absolute ──────────────────────────────────────────────────────────
{
id: "absolute_threshold",
name: "The Absolute Threshold",
description:
"The beginning of the end of everything. Your guild crosses it and feels, for the first time, that they have gone somewhere genuinely, ontologically final.",
status: "locked",
durationSeconds: 20 * 60 * 60,
rewards: [
{ type: "gold", amount: 8e95 },
{ type: "essence", amount: 2.5e92 },
{ type: "adventurer", targetId: "omniversal_champion" },
],
prerequisiteIds: [],
zoneId: "the_absolute",
},
{
id: "nothing_wastes",
name: "The Nothing Wastes",
description:
"Not empty — nothing. A region where even the concept of region is a courtesy your guild extends to the space by thinking about it. The moment they stop thinking, it stops being a space.",
status: "locked",
durationSeconds: 36 * 60 * 60,
rewards: [
{ type: "gold", amount: 4e101 },
{ type: "essence", amount: 1.2e98 },
{ type: "crystals", amount: 6e93 },
],
prerequisiteIds: ["absolute_threshold"],
zoneId: "the_absolute",
},
{
id: "final_paradox",
name: "The Final Paradox",
description:
"A region that exists by virtue of containing the contradiction of existence and non-existence simultaneously — a place that is also not a place, navigable only by those who have stopped needing either to be true.",
status: "locked",
durationSeconds: 56 * 60 * 60,
rewards: [
{ type: "gold", amount: 2e108 },
{ type: "essence", amount: 6e104 },
{ type: "crystals", amount: 3e100 },
{ type: "upgrade", targetId: "reality_warden_1" },
],
prerequisiteIds: ["nothing_wastes"],
zoneId: "the_absolute",
},
{
id: "end_vault",
name: "The Vault of Ends",
description:
"Everything that has ever ended is stored here — every life, every civilisation, every universe, every concept that has run its course. The collection is comprehensive. Your guild is not in it yet.",
status: "locked",
durationSeconds: 80 * 60 * 60,
rewards: [
{ type: "gold", amount: 1e115 },
{ type: "essence", amount: 3e111 },
{ type: "crystals", amount: 1.5e107 },
],
prerequisiteIds: ["final_paradox"],
zoneId: "the_absolute",
},
{
id: "terminal_approach",
name: "The Terminal Approach",
description:
"The last path before the last thing. Every step here is a step that has never been taken before and will never be taken again. The Absolute awaits at the end of it, and it is aware of your guild.",
status: "locked",
durationSeconds: 120 * 60 * 60,
rewards: [
{ type: "gold", amount: 5e121 },
{ type: "essence", amount: 1.5e118 },
{ type: "crystals", amount: 7e113 },
{ type: "upgrade", targetId: "infinity_ranger_1" },
],
prerequisiteIds: ["end_vault"],
zoneId: "the_absolute",
},
{
id: "absolute_dominion",
name: "Absolute Dominion",
description:
"This is it. Not the throne — not power — not victory. Just the knowledge, confirmed and total, that your guild reached the end of everything and was not ended. That is, in every measurable way, enough.",
status: "locked",
durationSeconds: 168 * 60 * 60,
rewards: [
{ type: "gold", amount: 3e130 },
{ type: "essence", amount: 9e126 },
{ type: "crystals", amount: 4e122 },
],
prerequisiteIds: ["terminal_approach"],
zoneId: "the_absolute",
},
];
+143
View File
@@ -580,4 +580,147 @@ export const DEFAULT_UPGRADES: Upgrade[] = [
purchased: false,
unlocked: false,
},
{
id: "aether_weaver_1",
name: "Aetheric Mastery",
description: "Complete mastery of aetheric forces doubles the weaver's output.",
target: "adventurer",
adventurerId: "aether_weaver",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 200_000_000,
purchased: false,
unlocked: false,
},
{
id: "titan_warrior_1",
name: "Titanic Fury",
description: "The fury of a titan unleashed — warrior effectiveness doubled.",
target: "adventurer",
adventurerId: "titan_warrior",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 700_000_000,
purchased: false,
unlocked: false,
},
{
id: "nexus_sage_1",
name: "Nexus Convergence",
description: "The sage converges all ley lines through their body — output doubled.",
target: "adventurer",
adventurerId: "nexus_sage",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 2_500_000_000,
purchased: false,
unlocked: false,
},
{
id: "cosmos_knight_1",
name: "Cosmic Tempering",
description: "Tempered by the heat of dying stars, the knight's effectiveness is doubled.",
target: "adventurer",
adventurerId: "cosmos_knight",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 9_000_000_000,
purchased: false,
unlocked: false,
},
{
id: "astral_sovereign_1",
name: "Sovereign Ascension",
description: "Ascension to true sovereignty over the astral plane doubles output.",
target: "adventurer",
adventurerId: "astral_sovereign",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 3e10,
purchased: false,
unlocked: false,
},
{
id: "primordial_mage_1",
name: "Primordial Awakening",
description: "Awakening of the mage's primordial heritage doubles their power.",
target: "adventurer",
adventurerId: "primordial_mage",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 1e11,
purchased: false,
unlocked: false,
},
{
id: "reality_warden_1",
name: "Reality Binding",
description: "The warden binds themselves to the structure of reality — effectiveness doubled.",
target: "adventurer",
adventurerId: "reality_warden",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 4e11,
purchased: false,
unlocked: false,
},
{
id: "infinity_ranger_1",
name: "Infinite Aim",
description: "The ranger's arrows travel through infinity itself — output doubled.",
target: "adventurer",
adventurerId: "infinity_ranger",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 1.5e12,
purchased: false,
unlocked: false,
},
{
id: "oblivion_paladin_1",
name: "Oblivion Consecration",
description: "Consecrated by the void between all things — paladin effectiveness doubled.",
target: "adventurer",
adventurerId: "oblivion_paladin",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 5e12,
purchased: false,
unlocked: false,
},
{
id: "transcendent_rogue_1",
name: "Transcendent Shadow",
description: "The rogue becomes one with the space between states — effectiveness doubled.",
target: "adventurer",
adventurerId: "transcendent_rogue",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 2e13,
purchased: false,
unlocked: false,
},
{
id: "omniversal_champion_1",
name: "Omniversal Dominion",
description: "Dominion over all versions of all universes — champion output doubled.",
target: "adventurer",
adventurerId: "omniversal_champion",
multiplier: 2,
costGold: 0,
costEssence: 0,
costCrystals: 8e13,
purchased: false,
unlocked: false,
},
];
+60
View File
@@ -121,4 +121,64 @@ export const DEFAULT_ZONES: Zone[] = [
unlockBossId: "void_emperor",
unlockQuestId: "heart_of_void",
},
{
id: "primordial_chaos",
name: "The Primordial Chaos",
description:
"Beyond the throne lies the raw stuff of creation itself — not a place but an ongoing argument between existence and non-existence that has never been resolved. Your guild enters the argument.",
emoji: "🌪️",
status: "locked",
unlockBossId: "the_apex",
unlockQuestId: "eternal_dominion",
},
{
id: "infinite_expanse",
name: "The Infinite Expanse",
description:
"A realm without edges, without centre, without reference — where distance is a concept that does not apply and your guild must define their own coordinates to navigate at all. Everything here is further than it looks.",
emoji: "♾️",
status: "locked",
unlockBossId: "primordial_titan",
unlockQuestId: "chaos_chronicle",
},
{
id: "reality_forge",
name: "The Reality Forge",
description:
"The workshop where the original universe was hammered into shape — still hot, still humming, still producing realities as a byproduct of its idle operation. The things that work here have never stopped.",
emoji: "⚒️",
status: "locked",
unlockBossId: "expanse_sovereign",
unlockQuestId: "expanse_codex",
},
{
id: "cosmic_maelstrom",
name: "The Cosmic Maelstrom",
description:
"A confluence of every force in existence, spinning in patterns that reduce galaxies to debris. Your guild navigates currents of energy that, on a good day, merely shatter planets.",
emoji: "🌀",
status: "locked",
unlockBossId: "reality_architect",
unlockQuestId: "forge_chronicle",
},
{
id: "primeval_sanctum",
name: "The Primeval Sanctum",
description:
"The oldest place that has ever existed — older than time, older than space, older than the concept of age itself. It holds something that remembers the moment before the first moment.",
emoji: "🗿",
status: "locked",
unlockBossId: "cosmic_annihilator",
unlockQuestId: "maelstrom_codex",
},
{
id: "the_absolute",
name: "The Absolute",
description:
"There is nothing beyond this. Not because nothing has been found — because nothing exists to find. The Absolute is the final truth: the end of all things that are and the beginning of all things that never were. Your guild stands at the edge of everything.",
emoji: "⚫",
status: "locked",
unlockBossId: "primeval_god",
unlockQuestId: "sanctum_chronicle",
},
];
+8 -4
View File
@@ -6,7 +6,7 @@ import { DEFAULT_ACHIEVEMENTS } from "../data/achievements.js";
import { DEFAULT_ADVENTURERS } from "../data/adventurers.js";
import { DEFAULT_EQUIPMENT } from "../data/equipment.js";
import { authMiddleware } from "../middleware/auth.js";
import { calculateOfflineGold } from "../services/offlineProgress.js";
import { calculateOfflineEarnings } from "../services/offlineProgress.js";
const RESOURCE_CAP = 1e300;
@@ -316,16 +316,20 @@ gameRouter.get("/load", async (context) => {
const now = Date.now();
const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now);
const { offlineGold, offlineEssence, offlineSeconds } = calculateOfflineEarnings(state, now);
if (offlineGold > 0) {
state.resources.gold += offlineGold;
state.player.totalGoldEarned += offlineGold;
}
if (offlineEssence > 0) {
state.resources.essence += offlineEssence;
}
state.lastTickAt = now;
if (needsBackfill || offlineGold > 0) {
if (needsBackfill || offlineGold > 0 || offlineEssence > 0) {
await prisma.gameState.update({
where: { discordId },
data: { state: state as object, updatedAt: now },
@@ -334,7 +338,7 @@ gameRouter.get("/load", async (context) => {
const secret = process.env.ANTI_CHEAT_SECRET;
const signature = secret ? computeHmac(JSON.stringify(state), secret) : undefined;
return context.json({ state, offlineGold, offlineSeconds, signature });
return context.json({ state, offlineGold, offlineEssence, offlineSeconds, signature });
});
gameRouter.post("/save", async (context) => {
+60 -1
View File
@@ -1,9 +1,11 @@
import type { GameState, PrestigeRequest } from "@elysium/types";
import type { BuyPrestigeUpgradeRequest, GameState, PrestigeRequest } from "@elysium/types";
import { Hono } from "hono";
import { prisma } from "../db/client.js";
import { authMiddleware } from "../middleware/auth.js";
import { DEFAULT_PRESTIGE_UPGRADES } from "../data/prestigeUpgrades.js";
import {
buildPostPrestigeState,
computeRunestoneMultipliers,
isEligibleForPrestige,
} from "../services/prestige.js";
@@ -61,3 +63,60 @@ prestigeRouter.post("/", async (context) => {
newPrestigeCount: newPrestigeData.count,
});
});
prestigeRouter.post("/buy-upgrade", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<BuyPrestigeUpgradeRequest>();
const { upgradeId } = body;
if (!upgradeId) {
return context.json({ error: "upgradeId is required" }, 400);
}
const upgrade = DEFAULT_PRESTIGE_UPGRADES.find((u) => u.id === upgradeId);
if (!upgrade) {
return context.json({ error: "Unknown prestige upgrade" }, 404);
}
const record = await prisma.gameState.findUnique({ where: { discordId } });
if (!record) {
return context.json({ error: "No save found" }, 404);
}
const state = record.state as unknown as GameState;
const { purchasedUpgradeIds, runestones } = state.prestige;
if (purchasedUpgradeIds.includes(upgradeId)) {
return context.json({ error: "Upgrade already purchased" }, 400);
}
if (runestones < upgrade.runestonesCost) {
return context.json({ error: "Not enough runestones" }, 400);
}
const newRunestones = runestones - upgrade.runestonesCost;
const newPurchasedUpgradeIds = [...purchasedUpgradeIds, upgradeId];
const newState: GameState = {
...state,
prestige: {
...state.prestige,
runestones: newRunestones,
purchasedUpgradeIds: newPurchasedUpgradeIds,
...computeRunestoneMultipliers(newPurchasedUpgradeIds),
},
};
await prisma.gameState.update({
where: { discordId },
data: { state: newState as object, updatedAt: Date.now() },
});
const multipliers = computeRunestoneMultipliers(newPurchasedUpgradeIds);
return context.json({
runestonesRemaining: newRunestones,
purchasedUpgradeIds: newPurchasedUpgradeIds,
...multipliers,
});
});
+33 -12
View File
@@ -3,21 +3,32 @@ import type { GameState } from "@elysium/types";
const MAX_OFFLINE_SECONDS = 8 * 60 * 60; // 8 hours
/**
* Calculates the gold earned whilst the player was offline.
* Calculates the gold and essence earned whilst the player was offline.
* Capped at 8 hours to prevent exploit via system clock manipulation.
* Applies the same multipliers as the client-side tick engine.
*/
export const calculateOfflineGold = (
export const calculateOfflineEarnings = (
state: GameState,
nowMs: number,
): { offlineGold: number; offlineSeconds: number } => {
): { offlineGold: number; offlineEssence: number; offlineSeconds: number } => {
const elapsedSeconds = Math.min(
(nowMs - state.lastTickAt) / 1000,
MAX_OFFLINE_SECONDS,
);
const goldPerSecond = state.adventurers.reduce((total, adventurer) => {
const equipmentGoldMultiplier = (state.equipment ?? [])
.filter((e) => e.equipped)
.reduce((mult, e) => mult * (e.bonus.goldMultiplier ?? 1), 1);
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
let goldPerSecond = 0;
let essencePerSecond = 0;
for (const adventurer of state.adventurers) {
if (!adventurer.unlocked || adventurer.count === 0) {
return total;
continue;
}
const upgradeMultiplier = state.upgrades
@@ -29,17 +40,27 @@ export const calculateOfflineGold = (
)
.reduce((mult, u) => mult * u.multiplier, 1);
return (
total +
const prestige = state.prestige.productionMultiplier;
goldPerSecond +=
adventurer.goldPerSecond *
adventurer.count *
upgradeMultiplier *
state.prestige.productionMultiplier
);
}, 0);
adventurer.count *
upgradeMultiplier *
prestige *
runestonesIncome *
equipmentGoldMultiplier;
essencePerSecond +=
adventurer.essencePerSecond *
adventurer.count *
upgradeMultiplier *
prestige *
runestonesEssence;
}
return {
offlineGold: goldPerSecond * elapsedSeconds,
offlineEssence: essencePerSecond * elapsedSeconds,
offlineSeconds: elapsedSeconds,
};
};
+57 -10
View File
@@ -1,26 +1,69 @@
import type { GameState, PrestigeData } from "@elysium/types";
import type {
GameState,
PrestigeData,
PrestigeUpgradeCategory,
} from "@elysium/types";
import { INITIAL_GAME_STATE } from "../data/initialState.js";
import { DEFAULT_PRESTIGE_UPGRADES } from "../data/prestigeUpgrades.js";
const PRESTIGE_GOLD_THRESHOLD = 1_000_000;
const BASE_PRESTIGE_GOLD_THRESHOLD = 1_000_000;
const THRESHOLD_SCALE_FACTOR = 5;
const RUNESTONES_PER_PRESTIGE_LEVEL = 10;
/**
* Calculates the gold threshold required for the next prestige.
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
*/
export const calculatePrestigeThreshold = (prestigeCount: number): number =>
BASE_PRESTIGE_GOLD_THRESHOLD * Math.pow(THRESHOLD_SCALE_FACTOR, prestigeCount);
export const isEligibleForPrestige = (state: GameState): boolean =>
state.player.totalGoldEarned >= PRESTIGE_GOLD_THRESHOLD;
state.player.totalGoldEarned >= calculatePrestigeThreshold(state.prestige.count);
const getCategoryMultiplier = (
purchasedUpgradeIds: string[],
category: PrestigeUpgradeCategory,
): number =>
DEFAULT_PRESTIGE_UPGRADES.filter(
(u) => u.category === category && purchasedUpgradeIds.includes(u.id),
).reduce((mult, u) => mult * u.multiplier, 1);
export const computeRunestoneMultipliers = (
purchasedUpgradeIds: string[],
): {
runestonesIncomeMultiplier: number;
runestonesClickMultiplier: number;
runestonesEssenceMultiplier: number;
runestonesCrystalMultiplier: number;
} => ({
runestonesIncomeMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "income"),
runestonesClickMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "click"),
runestonesEssenceMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "essence"),
runestonesCrystalMultiplier: getCategoryMultiplier(purchasedUpgradeIds, "crystals"),
});
/**
* Calculates how many runestones the player earns from a prestige.
* Formula: floor(sqrt(totalGoldEarned / PRESTIGE_GOLD_THRESHOLD)) * RUNESTONES_PER_PRESTIGE_LEVEL
* Formula: floor(sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL * runestoneMultiplier
*/
export const calculateRunestones = (totalGoldEarned: number): number =>
Math.floor(Math.sqrt(totalGoldEarned / PRESTIGE_GOLD_THRESHOLD)) *
RUNESTONES_PER_PRESTIGE_LEVEL;
export const calculateRunestones = (
totalGoldEarned: number,
prestigeCount: number,
purchasedUpgradeIds: string[],
): number => {
const threshold = calculatePrestigeThreshold(prestigeCount);
const base =
Math.floor(Math.sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL;
const runestoneMult = getCategoryMultiplier(purchasedUpgradeIds, "runestones");
return Math.floor(base * runestoneMult);
};
/**
* Calculates the new prestige production multiplier.
* Formula: 1 + (prestigeCount * 0.1) — each prestige adds 10% global production.
* Formula: 1.15^prestigeCount — exponential scaling per prestige.
*/
export const calculateProductionMultiplier = (prestigeCount: number): number =>
1 + prestigeCount * 0.1;
Math.pow(1.15, prestigeCount);
/**
* Generates the reset game state after a prestige.
@@ -32,15 +75,19 @@ export const buildPostPrestigeState = (
): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number } => {
const runestonesEarned = calculateRunestones(
currentState.player.totalGoldEarned,
currentState.prestige.count,
currentState.prestige.purchasedUpgradeIds,
);
const newPrestigeCount = currentState.prestige.count + 1;
const { purchasedUpgradeIds } = currentState.prestige;
const newPrestigeData: PrestigeData = {
count: newPrestigeCount,
runestones: currentState.prestige.runestones + runestonesEarned,
productionMultiplier: calculateProductionMultiplier(newPrestigeCount),
purchasedUpgradeIds: currentState.prestige.purchasedUpgradeIds,
purchasedUpgradeIds,
lastPrestigedAt: Date.now(),
...computeRunestoneMultipliers(purchasedUpgradeIds),
};
const freshState = INITIAL_GAME_STATE(currentState.player, characterName);
+10
View File
@@ -2,6 +2,8 @@ import type {
AuthResponse,
BossChallengeRequest,
BossChallengeResponse,
BuyPrestigeUpgradeRequest,
BuyPrestigeUpgradeResponse,
LoadResponse,
PrestigeRequest,
PrestigeResponse,
@@ -77,6 +79,14 @@ export const prestige = async (body: PrestigeRequest): Promise<PrestigeResponse>
body: JSON.stringify(body),
});
export const buyPrestigeUpgrade = async (
body: BuyPrestigeUpgradeRequest,
): Promise<BuyPrestigeUpgradeResponse> =>
request<BuyPrestigeUpgradeResponse>("/prestige/buy-upgrade", {
method: "POST",
body: JSON.stringify(body),
});
export const getPublicProfile = async (
discordId: string,
): Promise<PublicProfileResponse> =>
+13 -6
View File
@@ -1,18 +1,25 @@
import { useGame } from "../../context/GameContext.js";
export const OfflineModal = (): React.JSX.Element | null => {
const { offlineGold, dismissOfflineGold } = useGame();
const { offlineGold, offlineEssence, dismissOfflineGold, formatNumber } = useGame();
if (offlineGold <= 0) return null;
if (offlineGold <= 0 && offlineEssence <= 0) return null;
return (
<div className="modal-overlay">
<div className="modal">
<h2>Welcome back!</h2>
<p>
Your adventurers kept working whilst you were away and earned{" "}
<strong>🪙 {offlineGold.toFixed(0)} gold</strong>!
</p>
<p>Your adventurers kept working whilst you were away and earned:</p>
{offlineGold > 0 && (
<p>
<strong>🪙 {formatNumber(offlineGold)} gold</strong>
</p>
)}
{offlineEssence > 0 && (
<p>
<strong> {formatNumber(offlineEssence)} essence</strong>
</p>
)}
<p className="modal-note">Offline progress is calculated up to 8 hours.</p>
<button
className="modal-close-button"
+198 -52
View File
@@ -1,90 +1,236 @@
import type { PrestigeUpgradeCategory } from "@elysium/types";
import { useState } from "react";
import { prestige } from "../../api/client.js";
import { useGame } from "../../context/GameContext.js";
import {
PRESTIGE_UPGRADES,
PRESTIGE_UPGRADE_CATEGORY_LABELS,
} from "../../data/prestigeUpgrades.js";
const PRESTIGE_THRESHOLD = 1_000_000;
const BASE_THRESHOLD = 1_000_000;
const THRESHOLD_SCALE = 5;
const RUNESTONES_PER_LEVEL = 10;
const calculateThreshold = (prestigeCount: number): number =>
BASE_THRESHOLD * Math.pow(THRESHOLD_SCALE, prestigeCount);
const calculateProductionMultiplier = (prestigeCount: number): number =>
Math.pow(1.15, prestigeCount);
const calculateRunestonePreview = (
totalGoldEarned: number,
prestigeCount: number,
purchasedUpgradeIds: string[],
): number => {
const threshold = calculateThreshold(prestigeCount);
const base = Math.floor(Math.sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_LEVEL;
const runestoneMult = PRESTIGE_UPGRADES
.filter((u) => u.category === "runestones" && purchasedUpgradeIds.includes(u.id))
.reduce((mult, u) => mult * u.multiplier, 1);
return Math.floor(base * runestoneMult);
};
const CATEGORY_ORDER: PrestigeUpgradeCategory[] = [
"income",
"click",
"essence",
"crystals",
"runestones",
];
export const PrestigePanel = (): React.JSX.Element => {
const { state, reload, formatNumber } = useGame();
const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame();
const [characterName, setCharacterName] = useState("");
const [isPending, setIsPending] = useState(false);
const [result, setResult] = useState<{ runestones: number; count: number } | null>(null);
const [error, setError] = useState<string | null>(null);
const [prestigeError, setPrestigeError] = useState<string | null>(null);
const [buyingId, setBuyingId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<"prestige" | "shop">("prestige");
if (!state) return <section className="panel"><p>Loading...</p></section>;
const isEligible = state.player.totalGoldEarned >= PRESTIGE_THRESHOLD;
const { prestige: prestigeData, player } = state;
const threshold = calculateThreshold(prestigeData.count);
const isEligible = player.totalGoldEarned >= threshold;
const runestonePreview = calculateRunestonePreview(
player.totalGoldEarned,
prestigeData.count,
prestigeData.purchasedUpgradeIds,
);
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
const handlePrestige = async (): Promise<void> => {
if (!characterName.trim()) return;
setIsPending(true);
setError(null);
setPrestigeError(null);
try {
const data = await prestige({ characterName: characterName.trim() });
setResult({ runestones: data.runestones, count: data.newPrestigeCount });
await reload();
} catch (err) {
setError(err instanceof Error ? err.message : "Prestige failed");
setPrestigeError(err instanceof Error ? err.message : "Prestige failed");
} finally {
setIsPending(false);
}
};
const handleBuyUpgrade = async (upgradeId: string): Promise<void> => {
setBuyingId(upgradeId);
try {
await buyPrestigeUpgrade(upgradeId);
} finally {
setBuyingId(null);
}
};
const upgradesByCategory = CATEGORY_ORDER.map((category) => ({
category,
label: PRESTIGE_UPGRADE_CATEGORY_LABELS[category] ?? category,
upgrades: PRESTIGE_UPGRADES.filter((u) => u.category === category),
}));
return (
<section className="panel prestige-panel">
<h2> Prestige</h2>
<p>
Prestige resets your progress but grants <strong>Runestones</strong> permanent
currency used for powerful upgrades. Each prestige also increases your global
production multiplier by 10%.
</p>
<div className="prestige-status">
<p>
Total gold earned:{" "}
<strong>{formatNumber(state.player.totalGoldEarned)}</strong>
</p>
<p>
Required: <strong>{formatNumber(PRESTIGE_THRESHOLD)}</strong>
</p>
<p>Current prestige count: <strong>{state.prestige.count}</strong></p>
<p>
Production multiplier:{" "}
<strong>×{state.prestige.productionMultiplier.toFixed(1)}</strong>
</p>
<div className="prestige-tabs">
<button
className={`prestige-tab ${activeTab === "prestige" ? "active" : ""}`}
onClick={() => { setActiveTab("prestige"); }}
type="button"
>
Ascend
</button>
<button
className={`prestige-tab ${activeTab === "shop" ? "active" : ""}`}
onClick={() => { setActiveTab("shop"); }}
type="button"
>
🔮 Runestone Shop ({formatNumber(prestigeData.runestones)} stones)
</button>
</div>
{isEligible ? (
<div className="prestige-form">
<p>You are ready to prestige! Choose your new character name:</p>
<input
type="text"
value={characterName}
onChange={(e) => { setCharacterName(e.target.value); }}
placeholder="Character name..."
maxLength={32}
disabled={isPending}
/>
<button
className="prestige-button"
onClick={() => { void handlePrestige(); }}
disabled={isPending || !characterName.trim()}
type="button"
>
{isPending ? "Ascending..." : "✨ Ascend"}
</button>
{error && <p className="error">{error}</p>}
{result && (
<p className="success">
Ascended to Prestige {result.count}! Earned {result.runestones} Runestones.
{activeTab === "prestige" && (
<>
<p>
Prestige resets your progress but grants <strong>Runestones</strong> permanent
currency used for powerful upgrades. Each prestige multiplies your global production
by ×1.15 (compounding each run).
</p>
<div className="prestige-status">
<p>
Total gold this run:{" "}
<strong>{formatNumber(player.totalGoldEarned)}</strong>
</p>
<p>
Required to prestige: <strong>{formatNumber(threshold)}</strong>
</p>
<p>
Prestige count: <strong>{prestigeData.count}</strong>
</p>
<p>
Current production multiplier:{" "}
<strong>×{prestigeData.productionMultiplier.toFixed(2)}</strong>
</p>
<p>
After next prestige:{" "}
<strong>×{nextMultiplier.toFixed(2)}</strong>
</p>
<p>
Runestones: <strong>{formatNumber(prestigeData.runestones)}</strong>
</p>
{isEligible && (
<p className="runestone-preview">
Runestones on prestige: <strong>+{formatNumber(runestonePreview)}</strong>
</p>
)}
{!isEligible && (
<p className="prestige-progress">
Progress: {formatNumber(player.totalGoldEarned)} / {formatNumber(threshold)}{" "}
({((player.totalGoldEarned / threshold) * 100).toFixed(1)}%)
</p>
)}
</div>
{isEligible ? (
<div className="prestige-form">
<p>You are ready to prestige! Choose your new character name:</p>
<input
disabled={isPending}
maxLength={32}
onChange={(e) => { setCharacterName(e.target.value); }}
placeholder="Character name..."
type="text"
value={characterName}
/>
<button
className="prestige-button"
disabled={isPending || !characterName.trim()}
onClick={() => { void handlePrestige(); }}
type="button"
>
{isPending ? "Ascending..." : `✨ Ascend (+${formatNumber(runestonePreview)} Runestones)`}
</button>
{prestigeError && <p className="error">{prestigeError}</p>}
{result && (
<p className="success">
Ascended to Prestige {result.count}! Earned {formatNumber(result.runestones)} Runestones.
</p>
)}
</div>
) : (
<p className="prestige-locked">
Earn {formatNumber(threshold - player.totalGoldEarned)} more gold to unlock prestige.
</p>
)}
</>
)}
{activeTab === "shop" && (
<div className="runestone-shop">
<p className="shop-balance">
Balance: <strong>{formatNumber(prestigeData.runestones)} Runestones</strong>
</p>
{upgradesByCategory.map(({ category, label, upgrades }) => (
<div key={category} className="shop-category">
<h3>{label}</h3>
<div className="shop-upgrades">
{upgrades.map((upgrade) => {
const purchased = prestigeData.purchasedUpgradeIds.includes(upgrade.id);
const canAfford = prestigeData.runestones >= upgrade.runestonesCost;
const isLoading = buyingId === upgrade.id;
return (
<div
key={upgrade.id}
className={`shop-upgrade-card ${purchased ? "purchased" : ""} ${!canAfford && !purchased ? "unaffordable" : ""}`}
>
<div className="shop-upgrade-info">
<h4>{upgrade.name}</h4>
<p>{upgrade.description}</p>
<p className="upgrade-cost">
{purchased ? "✅ Purchased" : `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}
</p>
</div>
{!purchased && (
<button
className="buy-upgrade-button"
disabled={!canAfford || isLoading || buyingId !== null}
onClick={() => { void handleBuyUpgrade(upgrade.id); }}
type="button"
>
{isLoading ? "Buying..." : "Buy"}
</button>
)}
</div>
);
})}
</div>
</div>
))}
</div>
) : (
<p className="prestige-locked">
Earn {formatNumber(PRESTIGE_THRESHOLD - state.player.totalGoldEarned)} more
gold to unlock prestige.
</p>
)}
</section>
);
+24 -2
View File
@@ -18,18 +18,27 @@ const questTimeRemaining = (quest: Quest): number => {
interface QuestCardProps {
quest: Quest;
partyCombatPower: number;
unlockHint?: string | undefined;
zoneHint?: string | undefined;
}
const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
const QuestCard = ({ quest, partyCombatPower, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
const { startQuest, formatNumber } = useGame();
const cpRequired = quest.combatPowerRequired ?? 0;
const meetsCP = partyCombatPower >= cpRequired;
return (
<div className={`quest-card quest-${quest.status}`}>
<div className="quest-info">
<h3>{quest.name}</h3>
<p>{quest.description}</p>
{cpRequired > 0 && (
<p className={`quest-cp-requirement ${meetsCP ? "cp-met" : "cp-unmet"}`}>
Requires {formatNumber(cpRequired)} Combat Power
{quest.status === "available" && (meetsCP ? " ✓" : ` (you have ${formatNumber(partyCombatPower)})`)}
</p>
)}
<div className="quest-rewards">
{quest.rewards.map((reward, index) => (
// eslint-disable-next-line react/no-array-index-key -- rewards have no unique id
@@ -54,7 +63,9 @@ const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.E
{quest.status === "available" && (
<button
className="start-quest-button"
disabled={!meetsCP}
onClick={() => { startQuest(quest.id); }}
title={meetsCP ? undefined : `Need ${formatNumber(cpRequired)} combat power`}
type="button"
>
Send Party ({formatDuration(quest.durationSeconds)})
@@ -78,6 +89,11 @@ export const QuestPanel = (): React.JSX.Element => {
if (!state) return <section className="panel"><p>Loading...</p></section>;
const partyCombatPower = state.adventurers.reduce(
(total, a) => total + a.combatPower * a.count,
0,
);
const zones = state.zones ?? [];
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
const lockedCount = zoneQuests.filter((q) => q.status === "locked").length;
@@ -124,7 +140,13 @@ export const QuestPanel = (): React.JSX.Element => {
<div className="quest-list">
{visibleQuests.map((quest) => (
<QuestCard key={quest.id} quest={quest} unlockHint={questUnlockHints.get(quest.id)} zoneHint={questZoneHints.get(quest.id)} />
<QuestCard
key={quest.id}
partyCombatPower={partyCombatPower}
quest={quest}
unlockHint={questUnlockHints.get(quest.id)}
zoneHint={questZoneHints.get(quest.id)}
/>
))}
{visibleQuests.length === 0 && (
<p className="empty-zone">No quests to show in this zone.</p>
+41 -2
View File
@@ -7,7 +7,12 @@ import {
useRef,
useState,
} from "react";
import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js";
import {
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
challengeBoss as challengeBossApi,
loadGame,
saveGame,
} from "../api/client.js";
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
import { formatNumber as formatNumberUtil } from "../utils/format.js";
@@ -47,7 +52,9 @@ interface GameContextValue {
syncError: string | null;
/** Offline gold earned on login */
offlineGold: number;
/** Dismiss the offline gold notification */
/** Offline essence earned on login */
offlineEssence: number;
/** Dismiss the offline earnings notification */
dismissOfflineGold: () => void;
/** Battle result to display in the modal (null when no battle pending) */
battleResult: BattleResult | null;
@@ -63,6 +70,8 @@ interface GameContextValue {
setNumberFormat: (format: NumberFormat) => void;
/** Format a number using the player's chosen notation style */
formatNumber: (value: number) => string;
/** Buy a prestige upgrade from the runestone shop */
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
}
const GameContext = createContext<GameContextValue | null>(null);
@@ -74,6 +83,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [offlineGold, setOfflineGold] = useState(0);
const [offlineEssence, setOfflineEssence] = useState(0);
const [battleResult, setBattleResult] = useState<BattleResult | null>(null);
const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
@@ -104,6 +114,9 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (data.offlineGold > 0) {
setOfflineGold(data.offlineGold);
}
if (data.offlineEssence > 0) {
setOfflineEssence(data.offlineEssence);
}
// Fetch number format preference from profile (fire-and-forget, non-blocking)
void fetch(`/api/profile/${data.state.player.discordId}`)
.then(async (res) => {
@@ -353,6 +366,29 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
});
}, []);
const buyPrestigeUpgrade = useCallback(async (upgradeId: string) => {
try {
const result = await buyPrestigeUpgradeApi({ upgradeId });
setState((prev) => {
if (!prev) return prev;
return {
...prev,
prestige: {
...prev.prestige,
runestones: result.runestonesRemaining,
purchasedUpgradeIds: result.purchasedUpgradeIds,
runestonesIncomeMultiplier: result.runestonesIncomeMultiplier,
runestonesClickMultiplier: result.runestonesClickMultiplier,
runestonesEssenceMultiplier: result.runestonesEssenceMultiplier,
runestonesCrystalMultiplier: result.runestonesCrystalMultiplier,
},
};
});
} catch {
// Silently ignore — server errors shouldn't crash the UI
}
}, []);
const challengeBoss = useCallback(async (bossId: string) => {
if (!stateRef.current) return;
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
@@ -464,6 +500,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const dismissOfflineGold = useCallback(() => {
setOfflineGold(0);
setOfflineEssence(0);
}, []);
const dismissBattle = useCallback(() => {
@@ -498,6 +535,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
forceSync,
syncError,
offlineGold,
offlineEssence,
dismissOfflineGold,
battleResult,
dismissBattle,
@@ -506,6 +544,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
numberFormat,
setNumberFormat,
formatNumber: boundFormatNumber,
buyPrestigeUpgrade,
}}
>
{children}
+209
View File
@@ -0,0 +1,209 @@
import type { PrestigeUpgrade } from "@elysium/types";
export const PRESTIGE_UPGRADES: PrestigeUpgrade[] = [
// ── Global Income Tiers ───────────────────────────────────────────────────
{
id: "income_1",
name: "Runestone Blessing I",
description: "The first runestone awakens dormant power in your guild. All production ×1.25.",
category: "income",
runestonesCost: 10,
multiplier: 1.25,
},
{
id: "income_2",
name: "Runestone Blessing II",
description: "Deeper runestone resonance amplifies your workforce. All production ×1.5.",
category: "income",
runestonesCost: 25,
multiplier: 1.5,
},
{
id: "income_3",
name: "Runestone Blessing III",
description: "The runes sing with accumulated wisdom. All production ×2.",
category: "income",
runestonesCost: 60,
multiplier: 2,
},
{
id: "income_4",
name: "Runic Surge I",
description: "Runestone energy surges through your guild's operations. All production ×3.",
category: "income",
runestonesCost: 150,
multiplier: 3,
},
{
id: "income_5",
name: "Runic Surge II",
description: "The surge intensifies, pushing limits thought impossible. All production ×5.",
category: "income",
runestonesCost: 350,
multiplier: 5,
},
{
id: "income_6",
name: "Runic Surge III",
description: "An overwhelming tide of runic energy floods your operations. All production ×10.",
category: "income",
runestonesCost: 800,
multiplier: 10,
},
{
id: "income_7",
name: "Ancient Inscription I",
description: "You decipher ancient runic inscriptions that unlock vast potential. All production ×25.",
category: "income",
runestonesCost: 2_000,
multiplier: 25,
},
{
id: "income_8",
name: "Ancient Inscription II",
description: "Deeper inscriptions reveal secrets of primordial power. All production ×50.",
category: "income",
runestonesCost: 5_000,
multiplier: 50,
},
{
id: "income_9",
name: "Ancient Inscription III",
description: "The full inscription blazes with world-shaping power. All production ×100.",
category: "income",
runestonesCost: 12_000,
multiplier: 100,
},
{
id: "income_10",
name: "Eternal Rune I",
description: "The oldest runes, carved before memory began, yield their secrets at last. All production ×500.",
category: "income",
runestonesCost: 30_000,
multiplier: 500,
},
{
id: "income_11",
name: "Eternal Rune II",
description: "Eternal runes resonate with the heartbeat of creation itself. All production ×1,000.",
category: "income",
runestonesCost: 80_000,
multiplier: 1_000,
},
// ── Click Power ───────────────────────────────────────────────────────────
{
id: "click_power_1",
name: "Runic Strike I",
description: "Infuse your personal strikes with runestone energy. Click power ×2.",
category: "click",
runestonesCost: 15,
multiplier: 2,
},
{
id: "click_power_2",
name: "Runic Strike II",
description: "Your strikes crackle with compounded runic force. Click power ×5.",
category: "click",
runestonesCost: 75,
multiplier: 5,
},
{
id: "click_power_3",
name: "Runic Strike III",
description: "Every click channels the weight of all your past lives. Click power ×20.",
category: "click",
runestonesCost: 400,
multiplier: 20,
},
{
id: "click_power_4",
name: "World-Breaker Click",
description: "A single click now carries the force of a falling empire. Click power ×100.",
category: "click",
runestonesCost: 2_500,
multiplier: 100,
},
// ── Essence Production ────────────────────────────────────────────────────
{
id: "essence_1",
name: "Essence Attunement I",
description: "Runestone resonance amplifies your essence gathering. Essence production ×2.",
category: "essence",
runestonesCost: 20,
multiplier: 2,
},
{
id: "essence_2",
name: "Essence Attunement II",
description: "Deep attunement draws essence from previously invisible sources. Essence production ×5.",
category: "essence",
runestonesCost: 120,
multiplier: 5,
},
{
id: "essence_3",
name: "Essence Attunement III",
description: "Your guild breathes essence as naturally as air. Essence production ×20.",
category: "essence",
runestonesCost: 700,
multiplier: 20,
},
{
id: "essence_4",
name: "Essence Attunement IV",
description: "Essence flows in torrents from every corner of every world. Essence production ×100.",
category: "essence",
runestonesCost: 4_000,
multiplier: 100,
},
// ── Crystal Production ────────────────────────────────────────────────────
{
id: "crystal_1",
name: "Crystal Resonance I",
description: "Runestones vibrate in harmony with crystal structures. Crystal rewards ×2.",
category: "crystals",
runestonesCost: 30,
multiplier: 2,
},
{
id: "crystal_2",
name: "Crystal Resonance II",
description: "The resonance deepens, shattering crystal barriers. Crystal rewards ×5.",
category: "crystals",
runestonesCost: 200,
multiplier: 5,
},
{
id: "crystal_3",
name: "Crystal Resonance III",
description: "Pure resonance crystallises reality into abundance. Crystal rewards ×25.",
category: "crystals",
runestonesCost: 1_200,
multiplier: 25,
},
// ── Runestone Meta-Upgrades ───────────────────────────────────────────────
{
id: "runestone_gain_1",
name: "Runic Legacy",
description: "Your runestone attunement grows with each prestige. Earn 25% more runestones from future prestiges.",
category: "runestones",
runestonesCost: 50,
multiplier: 1.25,
},
{
id: "runestone_gain_2",
name: "Eternal Legacy",
description: "Your legend transcends individual lifetimes. Earn 50% more runestones from future prestiges.",
category: "runestones",
runestonesCost: 500,
multiplier: 1.5,
},
];
export const PRESTIGE_UPGRADE_CATEGORY_LABELS: Record<string, string> = {
income: "🪙 Global Income",
click: "👆 Click Power",
essence: "✨ Essence Production",
crystals: "💎 Crystal Rewards",
runestones: "🔮 Runestone Gain",
};
+16 -2
View File
@@ -57,6 +57,10 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
1,
);
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
const runestonesCrystal = state.prestige.runestonesCrystalMultiplier ?? 1;
let goldGained = 0;
let essenceGained = 0;
@@ -81,6 +85,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
adventurer.count *
upgradeMultiplier *
prestige *
runestonesIncome *
equipmentGoldMultiplier *
deltaSeconds;
@@ -89,6 +94,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
adventurer.count *
upgradeMultiplier *
prestige *
runestonesEssence *
deltaSeconds;
}
@@ -117,7 +123,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
} else if (reward.type === "essence" && reward.amount != null) {
questEssence += reward.amount;
} else if (reward.type === "crystals" && reward.amount != null) {
questCrystals += reward.amount;
questCrystals += reward.amount * runestonesCrystal;
} else if (reward.type === "upgrade" && reward.targetId != null) {
updatedUpgrades = updatedUpgrades.map((u) =>
u.id === reward.targetId ? { ...u, unlocked: true } : u,
@@ -250,5 +256,13 @@ export const calculateClickPower = (state: GameState): number => {
.filter((e) => e.equipped && e.bonus.clickMultiplier != null)
.reduce((mult, e) => mult * (e.bonus.clickMultiplier ?? 1), 1);
return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier * equipmentClickMultiplier;
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
return (
state.baseClickPower *
clickMultiplier *
state.prestige.productionMultiplier *
runestonesClick *
equipmentClickMultiplier
);
};
+6
View File
@@ -10,6 +10,8 @@ export type {
AuthResponse,
BossChallengeRequest,
BossChallengeResponse,
BuyPrestigeUpgradeRequest,
BuyPrestigeUpgradeResponse,
LoadResponse,
PrestigeRequest,
PrestigeResponse,
@@ -29,6 +31,10 @@ export type {
export type { GameState } from "./interfaces/GameState.js";
export type { Player } from "./interfaces/Player.js";
export type { PrestigeData } from "./interfaces/Prestige.js";
export type {
PrestigeUpgrade,
PrestigeUpgradeCategory,
} from "./interfaces/PrestigeUpgrade.js";
export type {
Quest,
QuestReward,
+15
View File
@@ -24,6 +24,8 @@ export interface LoadResponse {
state: GameState;
/** Offline gold earned since last save (server-calculated) */
offlineGold: number;
/** Offline essence earned since last save (server-calculated) */
offlineEssence: number;
/** Seconds the player was offline (capped at 8 hours) */
offlineSeconds: number;
/** HMAC-SHA256 signature of the loaded state — store and include in next save request */
@@ -72,6 +74,19 @@ export interface PrestigeResponse {
newPrestigeCount: number;
}
export interface BuyPrestigeUpgradeRequest {
upgradeId: string;
}
export interface BuyPrestigeUpgradeResponse {
runestonesRemaining: number;
purchasedUpgradeIds: string[];
runestonesIncomeMultiplier: number;
runestonesClickMultiplier: number;
runestonesEssenceMultiplier: number;
runestonesCrystalMultiplier: number;
}
export interface PublicProfileResponse {
characterName: string;
username: string;
@@ -9,4 +9,12 @@ export interface PrestigeData {
purchasedUpgradeIds: string[];
/** Unix timestamp of last prestige */
lastPrestigedAt?: number;
/** Pre-computed multiplier from "income" runestone upgrades */
runestonesIncomeMultiplier?: number;
/** Pre-computed multiplier from "click" runestone upgrades */
runestonesClickMultiplier?: number;
/** Pre-computed multiplier from "essence" runestone upgrades */
runestonesEssenceMultiplier?: number;
/** Pre-computed multiplier from "crystals" runestone upgrades */
runestonesCrystalMultiplier?: number;
}
@@ -0,0 +1,16 @@
export type PrestigeUpgradeCategory =
| "income"
| "click"
| "essence"
| "crystals"
| "runestones";
export interface PrestigeUpgrade {
id: string;
name: string;
description: string;
category: PrestigeUpgradeCategory;
runestonesCost: number;
/** Multiplier applied when this upgrade is purchased */
multiplier: number;
}
+2
View File
@@ -23,4 +23,6 @@ export interface Quest {
prerequisiteIds: string[];
/** Zone this quest belongs to */
zoneId: string;
/** Minimum party combat power required to start this quest */
combatPowerRequired?: number;
}