feat: add exploration and crafting systems

Adds two new game systems: Exploration (scouts collect materials from
timed area runs) and Crafting (combine materials into permanent
multipliers). Includes 72 exploration areas, 54 materials, 36 recipes,
and 108 new Codex lore entries. Removes unused characterName requirement
from prestige/transcendence/apotheosis reset flows.
This commit is contained in:
2026-03-07 04:14:04 -08:00
committed by Naomi Carrigan
parent 2aa6362ad6
commit 6ddf8e0b43
35 changed files with 4722 additions and 94 deletions
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -1,8 +1,9 @@
import type { ApotheosisData, GameState, Player, PrestigeData, TranscendenceData } from "@elysium/types";
import type { ApotheosisData, ExplorationState, GameState, Player, PrestigeData, TranscendenceData } from "@elysium/types";
import { DEFAULT_ACHIEVEMENTS } from "./achievements.js";
import { DEFAULT_ADVENTURERS } from "./adventurers.js";
import { DEFAULT_BOSSES } from "./bosses.js";
import { DEFAULT_EQUIPMENT } from "./equipment.js";
import { DEFAULT_EXPLORATIONS } from "./explorations.js";
import { DEFAULT_QUESTS } from "./quests.js";
import { DEFAULT_UPGRADES } from "./upgrades.js";
import { DEFAULT_ZONES } from "./zones.js";
@@ -29,6 +30,19 @@ export const INITIAL_APOTHEOSIS: ApotheosisData = {
count: 0,
};
export const INITIAL_EXPLORATION: ExplorationState = {
areas: DEFAULT_EXPLORATIONS.map((area) => ({
id: area.id,
status: area.zoneId === "verdant_vale" ? "available" as const : "locked" as const,
})),
materials: [],
craftedRecipeIds: [],
craftedGoldMultiplier: 1,
craftedEssenceMultiplier: 1,
craftedClickMultiplier: 1,
craftedCombatMultiplier: 1,
};
export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameState => ({
player: {
...player,
@@ -54,4 +68,5 @@ export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameS
lastTickAt: Date.now(),
transcendence: { ...INITIAL_TRANSCENDENCE },
apotheosis: { ...INITIAL_APOTHEOSIS },
exploration: structuredClone(INITIAL_EXPLORATION),
});
+93
View File
@@ -0,0 +1,93 @@
import type { Material } from "@elysium/types";
export const DEFAULT_MATERIALS: Material[] = [
// Zone 1: verdant_vale
{ id: "verdant_sap", name: "Verdant Sap", description: "Sticky resin tapped from ancient heartwood trees. Smells faintly of spring rain and something older beneath.", zoneId: "verdant_vale", rarity: "common" },
{ id: "forest_crystal", name: "Forest Crystal", description: "A translucent gem found buried near the roots of old-growth trees. Pulses with gentle life energy when held.", zoneId: "verdant_vale", rarity: "uncommon" },
{ id: "elder_bark", name: "Elder Bark", description: "Bark from a tree that has stood since before the kingdom. Harder than iron and warmer to the touch than it has any right to be.", zoneId: "verdant_vale", rarity: "rare" },
// Zone 2: shattered_ruins
{ id: "ruin_dust", name: "Ruin Dust", description: "Fine powder ground from fallen masonry. Still carries traces of the civilisation it once was — if you know how to read the patterns.", zoneId: "shattered_ruins", rarity: "common" },
{ id: "cursed_fragment", name: "Cursed Fragment", description: "A shard of enchanted stonework. The enchantment is broken, but something lingers in the grain of the stone, waiting.", zoneId: "shattered_ruins", rarity: "uncommon" },
{ id: "dragonscale_chip", name: "Dragonscale Chip", description: "A fragment of scale shed during the elder dragon's long reign over the ruins. Resistant to fire and magic alike.", zoneId: "shattered_ruins", rarity: "rare" },
// Zone 3: frozen_peaks
{ id: "glacial_ice", name: "Glacial Ice", description: "Ice from a glacier that has not moved in ten thousand years. Impossibly cold and perfectly clear, with something almost visible within.", zoneId: "frozen_peaks", rarity: "common" },
{ id: "frost_crystal", name: "Frost Crystal", description: "A crystal that formed inside the glacier itself over millennia. It never melts, not even near fire.", zoneId: "frozen_peaks", rarity: "uncommon" },
{ id: "void_shard", name: "Void Shard", description: "A fragment of the reality tear. It hums with wrongness that the fingers instinctively recognise before the mind does.", zoneId: "frozen_peaks", rarity: "rare" },
// Zone 4: shadow_marshes
{ id: "marsh_root", name: "Marsh Root", description: "Roots from the strangler plants that thrive in the fog-choked depths. Toxic without extensive preparation. Worth it, usually.", zoneId: "shadow_marshes", rarity: "common" },
{ id: "shadow_essence", name: "Shadow Essence", description: "Distilled darkness, caught in a vial before it could dissipate. Heavy and cold, and absolutely lightless.", zoneId: "shadow_marshes", rarity: "uncommon" },
{ id: "cursed_bone", name: "Cursed Bone", description: "Bone from something that died in the marsh so long ago it has become part of it. The curse runs deep through the marrow.", zoneId: "shadow_marshes", rarity: "rare" },
// Zone 5: volcanic_depths
{ id: "magma_stone", name: "Magma Stone", description: "Cooled lava that retained its internal heat. Warm to the touch even centuries after solidifying from whatever it once was.", zoneId: "volcanic_depths", rarity: "common" },
{ id: "ember_crystal", name: "Ember Crystal", description: "A crystal grown in the heart of a cooling magma chamber. Burns without being consumed, endlessly.", zoneId: "volcanic_depths", rarity: "uncommon" },
{ id: "legendary_ore", name: "Legendary Ore", description: "Ore from a seam that the fire elementals guard jealously. What it forges into is extraordinary by any measure.", zoneId: "volcanic_depths", rarity: "rare" },
// Zone 6: astral_void
{ id: "stardust", name: "Stardust", description: "Particulate matter from dying stars, collected from the void between worlds. Glitters even in total darkness.", zoneId: "astral_void", rarity: "common" },
{ id: "astral_thread", name: "Astral Thread", description: "Filaments of solidified probability. Handle with care — they remember every possible future they passed through.", zoneId: "astral_void", rarity: "uncommon" },
{ id: "void_crystal", name: "Void Crystal", description: "A crystal that formed in the spaces between spaces. Technically exists in several places simultaneously. Don't think too hard about it.", zoneId: "astral_void", rarity: "rare" },
// Zone 7: celestial_reaches
{ id: "celestial_dust", name: "Celestial Dust", description: "Residue from the celestial host's passing. Warm as sunlight and infinitely patient, as if waiting for something to happen.", zoneId: "celestial_reaches", rarity: "common" },
{ id: "divine_fragment", name: "Divine Fragment", description: "A chip of something the celestials discarded as imperfect. By mortal standards, it is extraordinary beyond measure.", zoneId: "celestial_reaches", rarity: "uncommon" },
{ id: "choir_shard", name: "Choir Shard", description: "A crystallised harmonic from the celestial choir. Resonates with a sound felt in the chest rather than heard with the ears.", zoneId: "celestial_reaches", rarity: "rare" },
// Zone 8: abyssal_trench
{ id: "trench_coral", name: "Trench Coral", description: "Coral from the deepest trenches where no light reaches and no warmth remains. Black as the water around it.", zoneId: "abyssal_trench", rarity: "common" },
{ id: "pressure_gem", name: "Pressure Gem", description: "A gem compressed by aeons of unimaginable pressure at the bottom of all things. Impossibly dense for its size.", zoneId: "abyssal_trench", rarity: "uncommon" },
{ id: "ancient_tooth", name: "Ancient Tooth", description: "A tooth from whatever has been waiting in the trench since before your world was made. It is very large.", zoneId: "abyssal_trench", rarity: "rare" },
// Zone 9: infernal_court
{ id: "brimstone_flake", name: "Brimstone Flake", description: "Sulphur residue from the court's perpetual fires. The smell never fully fades, no matter how carefully it is stored.", zoneId: "infernal_court", rarity: "common" },
{ id: "demon_ichor", name: "Demon Ichor", description: "Extracted from the court's refuse. Corrosive, powerful, and deeply unpleasant in every measurable way.", zoneId: "infernal_court", rarity: "uncommon" },
{ id: "soul_residue", name: "Soul Residue", description: "What remains after a soul has been fully processed by the court. Carries faint echoes of what it was before.", zoneId: "infernal_court", rarity: "rare" },
// Zone 10: crystalline_spire
{ id: "prism_dust", name: "Prism Dust", description: "Ground from the spire's outer facets. Each particle contains a compressed possibility that has not yet resolved.", zoneId: "crystalline_spire", rarity: "common" },
{ id: "calculation_shard", name: "Calculation Shard", description: "A fragment of the spire's core intelligence. Still running calculations on something that may or may not have an answer.", zoneId: "crystalline_spire", rarity: "uncommon" },
{ id: "possibility_crystal", name: "Possibility Crystal", description: "A crystal that contains a future that never happened. Treat carefully. The future remembers being possible.", zoneId: "crystalline_spire", rarity: "rare" },
// Zone 11: void_sanctum
{ id: "null_matter", name: "Null Matter", description: "Matter that exists in the space between spaces. Lacks most standard properties in ways that should not be possible.", zoneId: "void_sanctum", rarity: "common" },
{ id: "resonance_fragment", name: "Resonance Fragment", description: "A shard of the call that drew your guild here. Still resonant, still reaching toward something none of you can name.", zoneId: "void_sanctum", rarity: "uncommon" },
{ id: "sanctum_core", name: "Sanctum Core", description: "From the heart of the sanctum itself. What it does is undefined. What it is cannot be satisfactorily described.", zoneId: "void_sanctum", rarity: "rare" },
// Zone 12: eternal_throne
{ id: "throne_dust", name: "Throne Dust", description: "Residue from the base of the eternal throne. Old beyond any measurement that applies to things your guild understands.", zoneId: "eternal_throne", rarity: "common" },
{ id: "crown_fragment", name: "Crown Fragment", description: "A chip from one of the throne's crown-like spires. Authority made into something your hands can hold.", zoneId: "eternal_throne", rarity: "uncommon" },
{ id: "eternity_splinter", name: "Eternity Splinter", description: "From the throne's arm. Carries the weight of every decision ever made here, compressed into splinter-form.", zoneId: "eternal_throne", rarity: "rare" },
// Zone 13: primordial_chaos
{ id: "chaos_fragment", name: "Chaos Fragment", description: "A solidified moment of chaos. Still undecided about its own properties, which change depending on how you look at it.", zoneId: "primordial_chaos", rarity: "common" },
{ id: "creation_shard", name: "Creation Shard", description: "A fragment from when something was being made here. What was being made is unclear. Something important, probably.", zoneId: "primordial_chaos", rarity: "uncommon" },
{ id: "primordial_essence", name: "Primordial Essence", description: "The raw stuff of creation, before it became anything specific. Handle with care. It wants to become things.", zoneId: "primordial_chaos", rarity: "rare" },
// Zone 14: infinite_expanse
{ id: "expanse_dust", name: "Expanse Dust", description: "Gathered from somewhere in the expanse. Direction is uncertain. Distance from the collection point is uncertain.", zoneId: "infinite_expanse", rarity: "common" },
{ id: "distance_crystal", name: "Distance Crystal", description: "A crystal that contains compressed distance. It weighs more than its size suggests. Much more. Do not drop it.", zoneId: "infinite_expanse", rarity: "uncommon" },
{ id: "infinity_shard", name: "Infinity Shard", description: "A fragment of the expanse's edge, which the expanse does not technically have. This should not exist. It does anyway.", zoneId: "infinite_expanse", rarity: "rare" },
// Zone 15: reality_forge
{ id: "forge_ash", name: "Forge Ash", description: "Ash from the forge's fires. Contains fragments of unrealised realities that never quite made it to existence.", zoneId: "reality_forge", rarity: "common" },
{ id: "creation_tool", name: "Creation Tool", description: "A worn tool left by whatever worked here before your universe existed. Still functional in ways that are difficult to explain.", zoneId: "reality_forge", rarity: "uncommon" },
{ id: "reality_shard", name: "Reality Shard", description: "A flawed reality, discarded by the forge as below standard. Still contains everything a universe needs.", zoneId: "reality_forge", rarity: "rare" },
// Zone 16: cosmic_maelstrom
{ id: "maelstrom_debris", name: "Maelstrom Debris", description: "Debris from a galaxy that got too close to the maelstrom. Compressed to a size your guild can actually carry.", zoneId: "cosmic_maelstrom", rarity: "common" },
{ id: "force_crystal", name: "Force Crystal", description: "A crystal that formed at the intersection of several fundamental forces that should never have been in the same place.", zoneId: "cosmic_maelstrom", rarity: "uncommon" },
{ id: "cosmic_fragment", name: "Cosmic Fragment", description: "A fragment from the maelstrom's eye. Impossibly calm. Whatever is at the centre has been there since the beginning.", zoneId: "cosmic_maelstrom", rarity: "rare" },
// Zone 17: primeval_sanctum
{ id: "ancient_dust", name: "Ancient Dust", description: "Dust from the oldest place. Has been here since before the concept of 'here' had been invented.", zoneId: "primeval_sanctum", rarity: "common" },
{ id: "memory_shard", name: "Memory Shard", description: "A shard of something that remembers the moment before the first moment. The memory is in the material itself.", zoneId: "primeval_sanctum", rarity: "uncommon" },
{ id: "primeval_relic", name: "Primeval Relic", description: "An artefact from the first thing to exist in this place. What it did is unknown. That it mattered is beyond doubt.", zoneId: "primeval_sanctum", rarity: "rare" },
// Zone 18: the_absolute
{ id: "absolute_fragment", name: "Absolute Fragment", description: "A fragment of the final truth. It is difficult to look at directly, and impossible to look away from once you start.", zoneId: "the_absolute", rarity: "common" },
{ id: "boundary_shard", name: "Boundary Shard", description: "From the edge of everything. On one side: everything. On the other: nothing. This is from the very boundary between them.", zoneId: "the_absolute", rarity: "uncommon" },
{ id: "omega_crystal", name: "Omega Crystal", description: "The last crystal. After this, there are no more. It knows this. You can tell from the way it sits in your hand.", zoneId: "the_absolute", rarity: "rare" },
];
+327
View File
@@ -0,0 +1,327 @@
import type { CraftingRecipe } from "@elysium/types";
export const DEFAULT_RECIPES: CraftingRecipe[] = [
// Zone 1: verdant_vale
{
id: "heartwood_tincture",
name: "Heartwood Tincture",
description: "Sap from ancient heartwood trees, refined and bound with forest crystal. The resulting tincture accelerates the flow of wealth through your guild in ways the alchemists cannot fully explain.",
zoneId: "verdant_vale",
requiredMaterials: [{ materialId: "verdant_sap", quantity: 5 }, { materialId: "forest_crystal", quantity: 3 }],
bonus: { type: "gold_income", value: 1.05 },
},
{
id: "elder_bark_shield",
name: "Elder Bark Shield",
description: "A ward fashioned from bark older than the kingdom. Its presence on the battlefield is not merely physical — something ancient watches through it.",
zoneId: "verdant_vale",
requiredMaterials: [{ materialId: "elder_bark", quantity: 2 }, { materialId: "verdant_sap", quantity: 8 }],
bonus: { type: "combat_power", value: 1.08 },
},
// Zone 2: shattered_ruins
{
id: "runic_binding",
name: "Runic Binding",
description: "The ruin dust and cursed fragments, carefully worked into a binding that borrows the essence-drawing power of the fallen civilisation's final enchantments.",
zoneId: "shattered_ruins",
requiredMaterials: [{ materialId: "ruin_dust", quantity: 8 }, { materialId: "cursed_fragment", quantity: 4 }],
bonus: { type: "essence_income", value: 1.05 },
},
{
id: "dragon_scale_charm",
name: "Dragon Scale Charm",
description: "A charm set with a chip of the elder dragon's scale. The dragon would be furious if he knew. He would also be impressed.",
zoneId: "shattered_ruins",
requiredMaterials: [{ materialId: "dragonscale_chip", quantity: 2 }, { materialId: "ruin_dust", quantity: 10 }],
bonus: { type: "gold_income", value: 1.08 },
},
// Zone 3: frozen_peaks
{
id: "glacial_lens",
name: "Glacial Lens",
description: "Glacial ice ground and shaped into a lens that clarifies and focuses. Holding it, your guild's actions become sharper, more precise, more effective per motion.",
zoneId: "frozen_peaks",
requiredMaterials: [{ materialId: "glacial_ice", quantity: 8 }, { materialId: "frost_crystal", quantity: 4 }],
bonus: { type: "click_power", value: 1.08 },
},
{
id: "void_fragment_amulet",
name: "Void Fragment Amulet",
description: "The void shard set in frost crystal, creating something that should not be possible: a stable window into the spaces between spaces. Profitable beyond what physics suggests.",
zoneId: "frozen_peaks",
requiredMaterials: [{ materialId: "void_shard", quantity: 2 }, { materialId: "frost_crystal", quantity: 6 }],
bonus: { type: "gold_income", value: 1.10 },
},
// Zone 4: shadow_marshes
{
id: "shadow_extract",
name: "Shadow Extract",
description: "Marsh roots processed with shadow essence into a refined compound that somehow makes the essence of things flow more freely toward your guild hall.",
zoneId: "shadow_marshes",
requiredMaterials: [{ materialId: "marsh_root", quantity: 8 }, { materialId: "shadow_essence", quantity: 4 }],
bonus: { type: "essence_income", value: 1.08 },
},
{
id: "cursed_focus",
name: "Cursed Focus",
description: "The cursed bone and shadow essence combined into a focus that doesn't so much help your party fight as make things afraid of them before the battle begins.",
zoneId: "shadow_marshes",
requiredMaterials: [{ materialId: "cursed_bone", quantity: 2 }, { materialId: "shadow_essence", quantity: 6 }],
bonus: { type: "combat_power", value: 1.10 },
},
// Zone 5: volcanic_depths
{
id: "magma_core_seal",
name: "Magma Core Seal",
description: "A seal forged in the volcanic depths, using the eternal heat of the magma stone and ember crystal to create something that burns wealth into existence continuously.",
zoneId: "volcanic_depths",
requiredMaterials: [{ materialId: "magma_stone", quantity: 8 }, { materialId: "ember_crystal", quantity: 4 }],
bonus: { type: "gold_income", value: 1.10 },
},
{
id: "elemental_ore_ingot",
name: "Elemental Ore Ingot",
description: "The legendary ore, smelted in the volcanic forges with magma stone as fuel. What emerges is something the fire elementals recognise — and fear slightly.",
zoneId: "volcanic_depths",
requiredMaterials: [{ materialId: "legendary_ore", quantity: 2 }, { materialId: "magma_stone", quantity: 10 }],
bonus: { type: "combat_power", value: 1.12 },
},
// Zone 6: astral_void
{
id: "star_chart",
name: "Star Chart",
description: "Stardust arranged along astral threads into a map of the void that somehow, impossibly, shows your guild where to press and how to press it for maximum effect.",
zoneId: "astral_void",
requiredMaterials: [{ materialId: "stardust", quantity: 10 }, { materialId: "astral_thread", quantity: 4 }],
bonus: { type: "click_power", value: 1.12 },
},
{
id: "void_crystal_matrix",
name: "Void Crystal Matrix",
description: "A void crystal suspended in a matrix of stardust — something that exists in several places simultaneously and draws gold from all of them at once.",
zoneId: "astral_void",
requiredMaterials: [{ materialId: "void_crystal", quantity: 2 }, { materialId: "stardust", quantity: 12 }],
bonus: { type: "gold_income", value: 1.12 },
},
// Zone 7: celestial_reaches
{
id: "celestial_lens",
name: "Celestial Lens",
description: "Celestial dust and divine fragments ground into a lens that sees the essence in all things and draws a portion of it — gently, as the celestials would prefer.",
zoneId: "celestial_reaches",
requiredMaterials: [{ materialId: "celestial_dust", quantity: 10 }, { materialId: "divine_fragment", quantity: 4 }],
bonus: { type: "essence_income", value: 1.12 },
},
{
id: "choir_resonator",
name: "Choir Resonator",
description: "A choir shard set in divine fragments, still humming with the celestial harmonic. The resonance makes gold flow in its direction — not compelled, simply invited.",
zoneId: "celestial_reaches",
requiredMaterials: [{ materialId: "choir_shard", quantity: 2 }, { materialId: "divine_fragment", quantity: 6 }],
bonus: { type: "gold_income", value: 1.15 },
},
// Zone 8: abyssal_trench
{
id: "pressure_forged_core",
name: "Pressure-Forged Core",
description: "Trench coral and pressure gem combined under conditions that should have destroyed both. The result is something that survived conditions nothing should survive, which is ideal for combat.",
zoneId: "abyssal_trench",
requiredMaterials: [{ materialId: "trench_coral", quantity: 10 }, { materialId: "pressure_gem", quantity: 4 }],
bonus: { type: "combat_power", value: 1.15 },
},
{
id: "ancient_fang_talisman",
name: "Ancient Fang Talisman",
description: "A talisman set with the ancient tooth, suspended in trench coral carvings. Your party fights differently with this at their chest. More deliberately. More completely.",
zoneId: "abyssal_trench",
requiredMaterials: [{ materialId: "ancient_tooth", quantity: 2 }, { materialId: "trench_coral", quantity: 12 }],
bonus: { type: "click_power", value: 1.15 },
},
// Zone 9: infernal_court
{
id: "court_seal",
name: "Court Seal",
description: "A seal of infernal court authority, forged from brimstone and ichor. The court doesn't know you have this. It's better that way. It does make trade extremely efficient.",
zoneId: "infernal_court",
requiredMaterials: [{ materialId: "brimstone_flake", quantity: 10 }, { materialId: "demon_ichor", quantity: 5 }],
bonus: { type: "gold_income", value: 1.15 },
},
{
id: "soul_bound_catalyst",
name: "Soul-Bound Catalyst",
description: "Soul residue and demon ichor worked into a catalyst that draws the essence from everything around it — gently, without drawing the court's attention. Usually.",
zoneId: "infernal_court",
requiredMaterials: [{ materialId: "soul_residue", quantity: 2 }, { materialId: "demon_ichor", quantity: 8 }],
bonus: { type: "essence_income", value: 1.15 },
},
// Zone 10: crystalline_spire
{
id: "prism_array",
name: "Prism Array",
description: "Prism dust and calculation shards assembled into an array that the spire's intelligence would call elegant, if it had aesthetic preferences, which it might.",
zoneId: "crystalline_spire",
requiredMaterials: [{ materialId: "prism_dust", quantity: 10 }, { materialId: "calculation_shard", quantity: 4 }],
bonus: { type: "click_power", value: 1.18 },
},
{
id: "possibility_engine",
name: "Possibility Engine",
description: "A possibility crystal contained within a calculation shard framework. It runs through every possible outcome of every guild action and finds the one with the highest gold yield.",
zoneId: "crystalline_spire",
requiredMaterials: [{ materialId: "possibility_crystal", quantity: 2 }, { materialId: "calculation_shard", quantity: 6 }],
bonus: { type: "gold_income", value: 1.18 },
},
// Zone 11: void_sanctum
{
id: "null_field_generator",
name: "Null Field Generator",
description: "Null matter and resonance fragments assembled into something that generates a field where the laws governing how hard things are to kill become negotiable.",
zoneId: "void_sanctum",
requiredMaterials: [{ materialId: "null_matter", quantity: 10 }, { materialId: "resonance_fragment", quantity: 4 }],
bonus: { type: "combat_power", value: 1.18 },
},
{
id: "sanctum_key",
name: "Sanctum Key",
description: "A sanctum core and resonance fragments shaped into a key to something. The essence flows through it like it was designed to carry essence, which it may have been.",
zoneId: "void_sanctum",
requiredMaterials: [{ materialId: "sanctum_core", quantity: 2 }, { materialId: "resonance_fragment", quantity: 6 }],
bonus: { type: "essence_income", value: 1.18 },
},
// Zone 12: eternal_throne
{
id: "crown_circlet",
name: "Crown Circlet",
description: "Throne dust pressed into throne dust-lacquered crown fragments, shaped into a circlet. Wearing it — metaphorically — makes gold accumulate with the inevitability of authority.",
zoneId: "eternal_throne",
requiredMaterials: [{ materialId: "throne_dust", quantity: 10 }, { materialId: "crown_fragment", quantity: 4 }],
bonus: { type: "gold_income", value: 1.20 },
},
{
id: "eternity_bound_ring",
name: "Eternity-Bound Ring",
description: "An eternity splinter set into crown fragments, shaped into a ring. What it binds is unclear. Whatever it is, it makes your party significantly harder to kill.",
zoneId: "eternal_throne",
requiredMaterials: [{ materialId: "eternity_splinter", quantity: 2 }, { materialId: "crown_fragment", quantity: 6 }],
bonus: { type: "combat_power", value: 1.20 },
},
// Zone 13: primordial_chaos
{
id: "chaos_lens",
name: "Chaos Lens",
description: "Chaos fragments and creation shards arranged into a lens that hasn't decided what it wants to focus on yet, which somehow makes every click land harder than it should.",
zoneId: "primordial_chaos",
requiredMaterials: [{ materialId: "chaos_fragment", quantity: 10 }, { materialId: "creation_shard", quantity: 4 }],
bonus: { type: "click_power", value: 1.20 },
},
{
id: "creation_core",
name: "Creation Core",
description: "Primordial essence held in a creation shard framework. It hums constantly. Gold flows toward it with the enthusiasm of something that wants to become something.",
zoneId: "primordial_chaos",
requiredMaterials: [{ materialId: "primordial_essence", quantity: 2 }, { materialId: "creation_shard", quantity: 6 }],
bonus: { type: "gold_income", value: 1.22 },
},
// Zone 14: infinite_expanse
{
id: "distance_coil",
name: "Distance Coil",
description: "Expanse dust wound around distance crystals into a coil that draws essence from distances too vast to measure, compressing it into something your guild can actually use.",
zoneId: "infinite_expanse",
requiredMaterials: [{ materialId: "expanse_dust", quantity: 10 }, { materialId: "distance_crystal", quantity: 4 }],
bonus: { type: "essence_income", value: 1.20 },
},
{
id: "infinity_prism",
name: "Infinity Prism",
description: "An infinity shard mounted in a distance crystal frame. The prism reflects gold from an infinite number of directions simultaneously. The math works out favourably.",
zoneId: "infinite_expanse",
requiredMaterials: [{ materialId: "infinity_shard", quantity: 2 }, { materialId: "distance_crystal", quantity: 6 }],
bonus: { type: "gold_income", value: 1.22 },
},
// Zone 15: reality_forge
{
id: "reality_ingot",
name: "Reality Ingot",
description: "Forge ash smelted with creation tools into an ingot of compressed reality. Your party hits harder when carrying it. Reality does not enjoy being concentrated like this.",
zoneId: "reality_forge",
requiredMaterials: [{ materialId: "forge_ash", quantity: 10 }, { materialId: "creation_tool", quantity: 4 }],
bonus: { type: "combat_power", value: 1.22 },
},
{
id: "universe_seed",
name: "Universe Seed",
description: "A reality shard carefully shaped with creation tools into something that could, theoretically, become a universe. Instead it makes your clicks unreasonably effective.",
zoneId: "reality_forge",
requiredMaterials: [{ materialId: "reality_shard", quantity: 2 }, { materialId: "creation_tool", quantity: 6 }],
bonus: { type: "click_power", value: 1.22 },
},
// Zone 16: cosmic_maelstrom
{
id: "force_lens",
name: "Force Lens",
description: "Maelstrom debris and force crystals ground into a lens at the intersection of fundamental forces. Gold flows toward it with the same inevitability that galaxies flow toward gravity.",
zoneId: "cosmic_maelstrom",
requiredMaterials: [{ materialId: "maelstrom_debris", quantity: 10 }, { materialId: "force_crystal", quantity: 4 }],
bonus: { type: "gold_income", value: 1.25 },
},
{
id: "maelstrom_eye",
name: "Maelstrom Eye",
description: "A cosmic fragment suspended in a force crystal matrix — a piece of the maelstrom's impossible calm, holding the eye of the storm. Essence accumulates in its vicinity.",
zoneId: "cosmic_maelstrom",
requiredMaterials: [{ materialId: "cosmic_fragment", quantity: 2 }, { materialId: "force_crystal", quantity: 6 }],
bonus: { type: "essence_income", value: 1.22 },
},
// Zone 17: primeval_sanctum
{
id: "ancient_memory_array",
name: "Ancient Memory Array",
description: "Ancient dust and memory shards arranged into something that remembers how to fight before fighting was invented. Your party benefits from this instinct enormously.",
zoneId: "primeval_sanctum",
requiredMaterials: [{ materialId: "ancient_dust", quantity: 10 }, { materialId: "memory_shard", quantity: 4 }],
bonus: { type: "combat_power", value: 1.25 },
},
{
id: "first_artefact",
name: "First Artefact",
description: "The primeval relic, set into a memory shard framework. What function it originally served is unknowable. In your guild's hands, it makes every action more deliberate and more powerful.",
zoneId: "primeval_sanctum",
requiredMaterials: [{ materialId: "primeval_relic", quantity: 2 }, { materialId: "memory_shard", quantity: 6 }],
bonus: { type: "click_power", value: 1.25 },
},
// Zone 18: the_absolute
{
id: "final_truth_lens",
name: "Final Truth Lens",
description: "Absolute fragments and boundary shards ground into a lens that sees to the end of all things — and in seeing, draws the wealth inherent in finality toward your guild.",
zoneId: "the_absolute",
requiredMaterials: [{ materialId: "absolute_fragment", quantity: 10 }, { materialId: "boundary_shard", quantity: 4 }],
bonus: { type: "gold_income", value: 1.30 },
},
{
id: "omega_convergence",
name: "Omega Convergence",
description: "The last omega crystal, set at the convergence of boundary shards in the precise arrangement of an ending. What it does to combat is what endings do to everything: make it final.",
zoneId: "the_absolute",
requiredMaterials: [{ materialId: "omega_crystal", quantity: 2 }, { materialId: "boundary_shard", quantity: 6 }],
bonus: { type: "combat_power", value: 1.30 },
},
];
+4
View File
@@ -6,6 +6,8 @@ import { aboutRouter } from "./routes/about.js";
import { apotheosisRouter } from "./routes/apotheosis.js";
import { authRouter } from "./routes/auth.js";
import { bossRouter } from "./routes/boss.js";
import { craftRouter } from "./routes/craft.js";
import { exploreRouter } from "./routes/explore.js";
import { gameRouter } from "./routes/game.js";
import { prestigeRouter } from "./routes/prestige.js";
import { transcendenceRouter } from "./routes/transcendence.js";
@@ -27,6 +29,8 @@ app.route("/about", aboutRouter);
app.route("/auth", authRouter);
app.route("/game", gameRouter);
app.route("/boss", bossRouter);
app.route("/explore", exploreRouter);
app.route("/craft", craftRouter);
app.route("/prestige", prestigeRouter);
app.route("/transcendence", transcendenceRouter);
app.route("/apotheosis", apotheosisRouter);
+3 -9
View File
@@ -1,4 +1,4 @@
import type { ApotheosisRequest, GameState } from "@elysium/types";
import type { GameState } from "@elysium/types";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
@@ -14,12 +14,6 @@ apotheosisRouter.use("*", authMiddleware);
apotheosisRouter.post("/", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<ApotheosisRequest>();
const characterName = body.characterName?.trim();
if (!characterName) {
return context.json({ error: "characterName is required" }, 400);
}
const record = await prisma.gameState.findUnique({ where: { discordId } });
if (!record) {
@@ -41,7 +35,7 @@ apotheosisRouter.post("/", async (context) => {
const runAdventurersRecruited = state.adventurers.reduce((sum, a) => sum + a.count, 0);
const runAchievementsUnlocked = (state.achievements ?? []).filter((a) => a.unlockedAt !== null).length;
const { newState, newApotheosisData } = buildPostApotheosisState(state, characterName);
const { newState, newApotheosisData } = buildPostApotheosisState(state, state.player.characterName);
const now = Date.now();
await prisma.gameState.update({
@@ -52,7 +46,7 @@ apotheosisRouter.post("/", async (context) => {
await prisma.player.update({
where: { discordId },
data: {
characterName,
characterName: state.player.characterName,
// Reset current-run counters
totalGoldEarned: 0,
totalClicks: 0,
+2 -1
View File
@@ -60,7 +60,8 @@ const calculatePartyStats = (
}
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier * echoCombatMultiplier;
const craftedCombatMultiplier = state.exploration?.craftedCombatMultiplier ?? 1;
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier * echoCombatMultiplier * craftedCombatMultiplier;
return { partyDPS, partyMaxHp };
};
+103
View File
@@ -0,0 +1,103 @@
import type { CraftRecipeRequest, CraftRecipeResponse, GameState } from "@elysium/types";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
import { DEFAULT_RECIPES } from "../data/recipes.js";
import { authMiddleware } from "../middleware/auth.js";
export const craftRouter = new Hono<HonoEnv>();
craftRouter.use("*", authMiddleware);
const recomputeCraftedMultipliers = (
craftedRecipeIds: string[],
): {
craftedGoldMultiplier: number;
craftedEssenceMultiplier: number;
craftedClickMultiplier: number;
craftedCombatMultiplier: number;
} => ({
craftedGoldMultiplier: DEFAULT_RECIPES
.filter((r) => craftedRecipeIds.includes(r.id) && r.bonus.type === "gold_income")
.reduce((mult, r) => mult * r.bonus.value, 1),
craftedEssenceMultiplier: DEFAULT_RECIPES
.filter((r) => craftedRecipeIds.includes(r.id) && r.bonus.type === "essence_income")
.reduce((mult, r) => mult * r.bonus.value, 1),
craftedClickMultiplier: DEFAULT_RECIPES
.filter((r) => craftedRecipeIds.includes(r.id) && r.bonus.type === "click_power")
.reduce((mult, r) => mult * r.bonus.value, 1),
craftedCombatMultiplier: DEFAULT_RECIPES
.filter((r) => craftedRecipeIds.includes(r.id) && r.bonus.type === "combat_power")
.reduce((mult, r) => mult * r.bonus.value, 1),
});
craftRouter.post("/", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<CraftRecipeRequest>();
const { recipeId } = body;
if (!recipeId) {
return context.json({ error: "recipeId is required" }, 400);
}
const recipe = DEFAULT_RECIPES.find((r) => r.id === recipeId);
if (!recipe) {
return context.json({ error: "Unknown recipe" }, 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;
if (!state.exploration) {
return context.json({ error: "No exploration state found" }, 400);
}
if (state.exploration.craftedRecipeIds.includes(recipeId)) {
return context.json({ error: "Recipe already crafted" }, 400);
}
// Verify the player has all required materials
for (const requirement of recipe.requiredMaterials) {
const material = state.exploration.materials.find((m) => m.materialId === requirement.materialId);
const quantity = material?.quantity ?? 0;
if (quantity < requirement.quantity) {
return context.json(
{ error: `Not enough ${requirement.materialId} (need ${String(requirement.quantity)}, have ${String(quantity)})` },
400,
);
}
}
// Deduct materials
for (const requirement of recipe.requiredMaterials) {
const material = state.exploration.materials.find((m) => m.materialId === requirement.materialId);
if (material) {
material.quantity -= requirement.quantity;
}
}
// Add recipe and recompute all multipliers from scratch
state.exploration.craftedRecipeIds.push(recipeId);
const newMultipliers = recomputeCraftedMultipliers(state.exploration.craftedRecipeIds);
state.exploration.craftedGoldMultiplier = newMultipliers.craftedGoldMultiplier;
state.exploration.craftedEssenceMultiplier = newMultipliers.craftedEssenceMultiplier;
state.exploration.craftedClickMultiplier = newMultipliers.craftedClickMultiplier;
state.exploration.craftedCombatMultiplier = newMultipliers.craftedCombatMultiplier;
await prisma.gameState.update({
where: { discordId },
data: { state: state as object, updatedAt: Date.now() },
});
const response: CraftRecipeResponse = {
recipeId,
bonusType: recipe.bonus.type,
bonusValue: recipe.bonus.value,
...newMultipliers,
};
return context.json(response);
});
+263
View File
@@ -0,0 +1,263 @@
import type {
ExploreCollectEventResult,
ExploreCollectRequest,
ExploreCollectResponse,
ExploreStartRequest,
ExploreStartResponse,
GameState,
} from "@elysium/types";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
import { DEFAULT_EXPLORATIONS } from "../data/explorations.js";
import { INITIAL_EXPLORATION } from "../data/initialState.js";
import { authMiddleware } from "../middleware/auth.js";
export const exploreRouter = new Hono<HonoEnv>();
exploreRouter.use("*", authMiddleware);
const NOTHING_PROBABILITY = 0.2;
const NOTHING_MESSAGES = [
"Your scouts searched thoroughly but found nothing of value.",
"The area yielded nothing remarkable this time.",
"Your scouts returned empty-handed.",
"A wasted journey — the area proved barren.",
"Nothing to show for the effort. Perhaps next time.",
];
const pickNothingMessage = (): string =>
NOTHING_MESSAGES[Math.floor(Math.random() * NOTHING_MESSAGES.length)] ?? NOTHING_MESSAGES[0]!;
exploreRouter.post("/start", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<ExploreStartRequest>();
const { areaId } = body;
if (!areaId) {
return context.json({ error: "areaId is required" }, 400);
}
const explorationArea = DEFAULT_EXPLORATIONS.find((a) => a.id === areaId);
if (!explorationArea) {
return context.json({ error: "Unknown exploration area" }, 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;
// Backfill exploration state for old saves that predate this feature
if (!state.exploration) {
state.exploration = structuredClone(INITIAL_EXPLORATION);
// Unlock areas for zones already unlocked in this save
for (const area of state.exploration.areas) {
const areaData = DEFAULT_EXPLORATIONS.find((e) => e.id === area.id);
if (!areaData) continue;
const zone = state.zones.find((z) => z.id === areaData.zoneId);
if (zone?.status === "unlocked") {
area.status = "available";
}
}
}
const zone = state.zones.find((z) => z.id === explorationArea.zoneId);
if (!zone || zone.status !== "unlocked") {
return context.json({ error: "Zone is not unlocked" }, 400);
}
const area = state.exploration.areas.find((a) => a.id === areaId);
if (!area) {
return context.json({ error: "Exploration area not found in state" }, 404);
}
if (area.status === "in_progress") {
return context.json({ error: "Exploration already in progress" }, 400);
}
if (area.status === "locked") {
return context.json({ error: "Exploration area is locked" }, 400);
}
const now = Date.now();
area.status = "in_progress";
area.startedAt = now;
await prisma.gameState.update({
where: { discordId },
data: { state: state as object, updatedAt: now },
});
const response: ExploreStartResponse = {
areaId,
endsAt: now + explorationArea.durationSeconds * 1000,
};
return context.json(response);
});
exploreRouter.post("/collect", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<ExploreCollectRequest>();
const { areaId } = body;
if (!areaId) {
return context.json({ error: "areaId is required" }, 400);
}
const explorationArea = DEFAULT_EXPLORATIONS.find((a) => a.id === areaId);
if (!explorationArea) {
return context.json({ error: "Unknown exploration area" }, 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;
if (!state.exploration) {
return context.json({ error: "No exploration state found" }, 400);
}
const area = state.exploration.areas.find((a) => a.id === areaId);
if (!area) {
return context.json({ error: "Exploration area not found" }, 404);
}
if (area.status !== "in_progress") {
return context.json({ error: "Exploration is not in progress" }, 400);
}
const now = Date.now();
const startedAt = area.startedAt ?? 0;
const durationMs = explorationArea.durationSeconds * 1000;
if (now < startedAt + durationMs) {
return context.json({ error: "Exploration is not yet complete" }, 400);
}
area.status = "available";
area.completedOnce = true;
// 20% chance of finding nothing
if (Math.random() < NOTHING_PROBABILITY) {
await prisma.gameState.update({
where: { discordId },
data: { state: state as object, updatedAt: now },
});
const response: ExploreCollectResponse = {
foundNothing: true,
nothingMessage: pickNothingMessage(),
materialsFound: [],
event: null,
};
return context.json(response);
}
// Pick a random event
const event = explorationArea.events[Math.floor(Math.random() * explorationArea.events.length)]!;
// Apply event effects and build the result summary
let goldChange = 0;
let essenceChange = 0;
let materialGained: { materialId: string; quantity: number } | null = null;
if (event.effect.type === "gold_gain") {
const amount = event.effect.amount ?? 0;
state.resources.gold += amount;
state.player.totalGoldEarned += amount;
goldChange = amount;
} else if (event.effect.type === "gold_loss") {
const amount = Math.min(state.resources.gold, event.effect.amount ?? 0);
state.resources.gold -= amount;
goldChange = -amount;
} else if (event.effect.type === "essence_gain") {
const amount = event.effect.amount ?? 0;
state.resources.essence += amount;
essenceChange = amount;
} else if (event.effect.type === "material_gain") {
const materialId = event.effect.materialId;
const quantity = event.effect.quantity ?? 1;
if (materialId) {
const existing = state.exploration.materials.find((m) => m.materialId === materialId);
if (existing) {
existing.quantity += quantity;
} else {
state.exploration.materials.push({ materialId, quantity });
}
materialGained = { materialId, quantity };
}
} else if (event.effect.type === "adventurer_loss") {
const fraction = event.effect.fraction ?? 0.05;
let totalLost = 0;
for (const adventurer of state.adventurers) {
const lost = Math.floor(adventurer.count * fraction);
if (lost > 0) {
adventurer.count = Math.max(0, adventurer.count - lost);
totalLost += lost;
}
}
// adventurerLostCount captured below
}
const adventurerLostCount =
event.effect.type === "adventurer_loss"
? state.adventurers.reduce((sum, a) => {
const fraction = event.effect.fraction ?? 0.05;
return sum + Math.floor(a.count * fraction);
}, 0)
: 0;
const eventResult: ExploreCollectEventResult = {
text: event.text,
goldChange,
essenceChange,
materialGained,
adventurerLostCount,
};
// Roll for material drops from possibleMaterials (weighted random selection)
const materialsFound: Array<{ materialId: string; quantity: number }> = [];
if (explorationArea.possibleMaterials.length > 0) {
const totalWeight = explorationArea.possibleMaterials.reduce((sum, m) => sum + m.weight, 0);
let roll = Math.random() * totalWeight;
for (const possible of explorationArea.possibleMaterials) {
roll -= possible.weight;
if (roll <= 0) {
const quantity =
Math.floor(Math.random() * (possible.maxQuantity - possible.minQuantity + 1)) +
possible.minQuantity;
const existing = state.exploration.materials.find((m) => m.materialId === possible.materialId);
if (existing) {
existing.quantity += quantity;
} else {
state.exploration.materials.push({ materialId: possible.materialId, quantity });
}
materialsFound.push({ materialId: possible.materialId, quantity });
break;
}
}
}
await prisma.gameState.update({
where: { discordId },
data: { state: state as object, updatedAt: now },
});
const response: ExploreCollectResponse = {
foundNothing: false,
materialsFound,
event: eventResult,
};
return context.json(response);
});
+67 -3
View File
@@ -9,6 +9,8 @@ import { DEFAULT_ADVENTURERS } from "../data/adventurers.js";
import { DEFAULT_BOSSES } from "../data/bosses.js";
import { DEFAULT_EQUIPMENT } from "../data/equipment.js";
import { DEFAULT_EQUIPMENT_SETS } from "../data/equipmentSets.js";
import { DEFAULT_EXPLORATIONS } from "../data/explorations.js";
import { INITIAL_EXPLORATION } from "../data/initialState.js";
import { DEFAULT_QUESTS } from "../data/quests.js";
import { authMiddleware } from "../middleware/auth.js";
import { getOrResetDailyChallenges } from "../services/dailyChallenges.js";
@@ -51,6 +53,8 @@ const computeMaxPassiveIncome = (
const setGoldMultiplier = computeSetBonuses(equippedItemIds, DEFAULT_EQUIPMENT_SETS).goldMultiplier;
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
const craftedGoldMultiplier = state.exploration?.craftedGoldMultiplier ?? 1;
const craftedEssenceMultiplier = state.exploration?.craftedEssenceMultiplier ?? 1;
let goldPerSecond = 0;
let essencePerSecond = 0;
@@ -76,14 +80,16 @@ const computeMaxPassiveIncome = (
prestige *
runestonesIncome *
equipmentGoldMultiplier *
setGoldMultiplier;
setGoldMultiplier *
craftedGoldMultiplier;
essencePerSecond +=
adventurer.essencePerSecond *
adventurer.count *
upgradeMultiplier *
prestige *
runestonesEssence;
runestonesEssence *
craftedEssenceMultiplier;
}
return { goldPerSecond, essencePerSecond };
@@ -309,7 +315,38 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat
? { apotheosis: previous.apotheosis }
: {};
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread, ...apotheosisSpread };
// Exploration: materials and crafted recipes can only be added server-side.
// Cap material quantities and crafted recipe IDs at the previous DB values to block inflation.
// Crafted multipliers are always derived from the previous state (only /craft can change them).
const explorationSpread = (() => {
const prevExploration = previous.exploration;
if (!incoming.exploration) {
return prevExploration ? { exploration: prevExploration } : {};
}
if (!prevExploration) {
return { exploration: incoming.exploration };
}
const materials = incoming.exploration.materials.map((m) => {
const prev = prevExploration.materials.find((p) => p.materialId === m.materialId);
return { ...m, quantity: Math.min(m.quantity, prev?.quantity ?? 0) };
});
const craftedRecipeIds = incoming.exploration.craftedRecipeIds.filter((id) =>
prevExploration.craftedRecipeIds.includes(id),
);
return {
exploration: {
...incoming.exploration,
materials,
craftedRecipeIds,
craftedGoldMultiplier: prevExploration.craftedGoldMultiplier,
craftedEssenceMultiplier: prevExploration.craftedEssenceMultiplier,
craftedClickMultiplier: prevExploration.craftedClickMultiplier,
craftedCombatMultiplier: prevExploration.craftedCombatMultiplier,
},
};
})();
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread, ...apotheosisSpread, ...explorationSpread };
};
export const gameRouter = new Hono<HonoEnv>();
@@ -559,6 +596,33 @@ gameRouter.get("/load", async (context) => {
}
}
// Backfill exploration state on saves that predate the feature
if (!state.exploration) {
state.exploration = structuredClone(INITIAL_EXPLORATION);
// Unlock areas for zones already unlocked in this save
for (const area of state.exploration.areas) {
const areaData = DEFAULT_EXPLORATIONS.find((e) => e.id === area.id);
if (!areaData) continue;
const zone = state.zones.find((z) => z.id === areaData.zoneId);
if (zone?.status === "unlocked") {
area.status = "available";
}
}
needsBackfill = true;
} else {
// Merge any new exploration areas added since this save was created
for (const defaultArea of DEFAULT_EXPLORATIONS) {
if (!state.exploration.areas.some((a) => a.id === defaultArea.id)) {
const zone = state.zones.find((z) => z.id === defaultArea.zoneId);
state.exploration.areas.push({
id: defaultArea.id,
status: zone?.status === "unlocked" ? "available" : "locked",
});
needsBackfill = true;
}
}
}
const now = Date.now();
const { offlineGold, offlineEssence, offlineSeconds } = calculateOfflineEarnings(state, now);
+3 -9
View File
@@ -1,4 +1,4 @@
import type { BuyPrestigeUpgradeRequest, GameState, PrestigeRequest } from "@elysium/types";
import type { BuyPrestigeUpgradeRequest, GameState } from "@elysium/types";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
@@ -17,12 +17,6 @@ prestigeRouter.use("*", authMiddleware);
prestigeRouter.post("/", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<PrestigeRequest>();
const characterName = body.characterName?.trim();
if (!characterName) {
return context.json({ error: "characterName is required" }, 400);
}
const record = await prisma.gameState.findUnique({ where: { discordId } });
@@ -50,7 +44,7 @@ prestigeRouter.post("/", async (context) => {
const { newState, newPrestigeData, runestonesEarned, milestoneRunestones } = buildPostPrestigeState(
state,
characterName,
state.player.characterName,
);
// Preserve daily challenges across the prestige reset and apply any crystal rewards
@@ -78,7 +72,7 @@ prestigeRouter.post("/", async (context) => {
await prisma.player.update({
where: { discordId },
data: {
characterName,
characterName: state.player.characterName,
// Reset current-run counters
totalGoldEarned: 0,
totalClicks: 0,
+3 -9
View File
@@ -1,4 +1,4 @@
import type { BuyEchoUpgradeRequest, GameState, TranscendenceRequest } from "@elysium/types";
import type { BuyEchoUpgradeRequest, GameState } from "@elysium/types";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
@@ -16,12 +16,6 @@ transcendenceRouter.use("*", authMiddleware);
transcendenceRouter.post("/", async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<TranscendenceRequest>();
const characterName = body.characterName?.trim();
if (!characterName) {
return context.json({ error: "characterName is required" }, 400);
}
const record = await prisma.gameState.findUnique({ where: { discordId } });
if (!record) {
@@ -39,7 +33,7 @@ transcendenceRouter.post("/", async (context) => {
const { newState, newTranscendenceData, echoesEarned } = buildPostTranscendenceState(
state,
characterName,
state.player.characterName,
);
// Capture current-run stats before the nuclear reset
@@ -57,7 +51,7 @@ transcendenceRouter.post("/", async (context) => {
await prisma.player.update({
where: { discordId },
data: {
characterName,
characterName: state.player.characterName,
// Reset current-run counters (same as prestige)
totalGoldEarned: 0,
totalClicks: 0,