diff --git a/apps/api/src/data/explorations.ts b/apps/api/src/data/explorations.ts new file mode 100644 index 0000000..6f73192 --- /dev/null +++ b/apps/api/src/data/explorations.ts @@ -0,0 +1,1245 @@ +import type { ExplorationArea } from "@elysium/types"; + +export const DEFAULT_EXPLORATIONS: ExplorationArea[] = [ + // ── Zone 1: verdant_vale ────────────────────────────────────────────────── + { + id: "verdant_meadow", + name: "The Verdant Meadow", + description: "Rolling fields of wildflowers at the edge of the guild's territory. Travellers pass through often, and occasionally leave things behind.", + zoneId: "verdant_vale", + durationSeconds: 3600, // 1h + possibleMaterials: [ + { materialId: "verdant_sap", minQuantity: 1, maxQuantity: 3, weight: 3 }, + ], + events: [ + { id: "vm_e1", text: "A passing merchant overcharged for his wares and your scouts recovered the difference. Gold gained.", effect: { type: "gold_gain", amount: 1000 } }, + { id: "vm_e2", text: "Bandits made off with a scout's supply pack before they could be stopped.", effect: { type: "gold_loss", amount: 500 } }, + { id: "vm_e3", text: "A nest of rare resin-producing beetles yields an extra harvest.", effect: { type: "material_gain", materialId: "verdant_sap", quantity: 2 } }, + { id: "vm_e4", text: "A group of wandering peasants heard of your guild's reputation and joined up.", effect: { type: "essence_gain", amount: 50 } }, + ], + }, + { + id: "whispering_forest", + name: "The Whispering Forest", + description: "Ancient trees whose canopy blocks out most of the light. The forest whispers things your scouts swear they understand, just not when they try to remember later.", + zoneId: "verdant_vale", + durationSeconds: 7200, // 2h + possibleMaterials: [ + { materialId: "verdant_sap", minQuantity: 2, maxQuantity: 5, weight: 3 }, + { materialId: "forest_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "wf_e1", text: "A hidden cache of coins, lost by some forgotten traveller, is found beneath a root.", effect: { type: "gold_gain", amount: 3000 } }, + { id: "wf_e2", text: "The forest's whispers led a scout too far from the path. Rescue cost time and coin.", effect: { type: "gold_loss", amount: 1500 } }, + { id: "wf_e3", text: "A particularly ancient tree yields an unusually dense crystal in its roots.", effect: { type: "material_gain", materialId: "forest_crystal", quantity: 1 } }, + { id: "wf_e4", text: "Something in the forest air sharpens the mind. The scouts return unusually focused.", effect: { type: "essence_gain", amount: 100 } }, + ], + }, + { + id: "ancient_grove", + name: "The Ancient Grove", + description: "A circle of trees so old they predate the kingdom. Druids once held ceremonies here. The trees remember, and their bark holds echoes of old power.", + zoneId: "verdant_vale", + durationSeconds: 10800, // 3h + possibleMaterials: [ + { materialId: "forest_crystal", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "verdant_sap", minQuantity: 1, maxQuantity: 3, weight: 2 }, + ], + events: [ + { id: "ag_e1", text: "The grove's old power draws fortune: a vein of gold-threaded rock runs beneath one of the roots.", effect: { type: "gold_gain", amount: 6000 } }, + { id: "ag_e2", text: "A territorial spirit drove off two scouts and damaged their equipment.", effect: { type: "gold_loss", amount: 2500 } }, + { id: "ag_e3", text: "Deep in the root system, an unusually large crystal cluster breaks off cleanly.", effect: { type: "material_gain", materialId: "forest_crystal", quantity: 2 } }, + { id: "ag_e4", text: "The ancient grove restores something that had been slowly depleted. The essence flows back.", effect: { type: "essence_gain", amount: 200 } }, + ], + }, + { + id: "forbidden_glen", + name: "The Forbidden Glen", + description: "A clearing the locals will not enter after dark. Something about the bark of the trees here is different. Your scouts feel watched the entire time.", + zoneId: "verdant_vale", + durationSeconds: 14400, // 4h + possibleMaterials: [ + { materialId: "forest_crystal", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "elder_bark", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "fg_e1", text: "Whatever watches the glen seems to approve of your guild. A gift of old coin is left at the entrance.", effect: { type: "gold_gain", amount: 10000 } }, + { id: "fg_e2", text: "Whatever watches the glen does not approve. Three scouts come back without their packs.", effect: { type: "gold_loss", amount: 4000 } }, + { id: "fg_e3", text: "A shard of elder bark falls, as if offered.", effect: { type: "material_gain", materialId: "elder_bark", quantity: 1 } }, + { id: "fg_e4", text: "The forbidden glen leaves a mark on your scouts — not unpleasant. Their focus sharpens.", effect: { type: "essence_gain", amount: 400 } }, + ], + }, + + // ── Zone 2: shattered_ruins ─────────────────────────────────────────────── + { + id: "collapsed_outpost", + name: "The Collapsed Outpost", + description: "What was once a military garrison, now half-buried in rubble and wild growth. The previous occupants left in a hurry and did not take everything.", + zoneId: "shattered_ruins", + durationSeconds: 7200, // 2h + possibleMaterials: [ + { materialId: "ruin_dust", minQuantity: 2, maxQuantity: 5, weight: 3 }, + ], + events: [ + { id: "co_e1", text: "A hidden armory beneath the rubble yields weapons worth selling.", effect: { type: "gold_gain", amount: 4000 } }, + { id: "co_e2", text: "A structural collapse pins two scouts briefly. Extraction costs.", effect: { type: "gold_loss", amount: 2000 } }, + { id: "co_e3", text: "The outpost's old enchantments left residue in the stonework, still harvestable.", effect: { type: "material_gain", materialId: "ruin_dust", quantity: 3 } }, + { id: "co_e4", text: "Old battle-essence still clings to the walls. Something can be drawn from it.", effect: { type: "essence_gain", amount: 150 } }, + ], + }, + { + id: "cursed_lake", + name: "The Cursed Lake", + description: "The water here reflects things that aren't there. Something is at the bottom that doesn't want to be found, which means your scouts want very much to find it.", + zoneId: "shattered_ruins", + durationSeconds: 14400, // 4h + possibleMaterials: [ + { materialId: "ruin_dust", minQuantity: 2, maxQuantity: 6, weight: 3 }, + { materialId: "cursed_fragment", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "cl_e1", text: "The lake yields sunken treasure from a caravan that tried to ford it centuries ago.", effect: { type: "gold_gain", amount: 10000 } }, + { id: "cl_e2", text: "The curse reaches out and sends three scouts home with rattled nerves and empty purses.", effect: { type: "gold_loss", amount: 4000 } }, + { id: "cl_e3", text: "Something at the lake's edge is not quite stone and not quite crystal.", effect: { type: "material_gain", materialId: "cursed_fragment", quantity: 1 } }, + { id: "cl_e4", text: "The curse is potent, but potency can be harvested. Your alchemist is pleased.", effect: { type: "essence_gain", amount: 300 } }, + ], + }, + { + id: "runic_archive", + name: "The Runic Archive", + description: "Buried walls covered in script no living scholar can read. The knowledge is lost but the enchantments remain, faded but still murmuring in the stone.", + zoneId: "shattered_ruins", + durationSeconds: 21600, // 6h + possibleMaterials: [ + { materialId: "cursed_fragment", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "ruin_dust", minQuantity: 2, maxQuantity: 4, weight: 2 }, + ], + events: [ + { id: "ra_e1", text: "A readable passage in the archive describes the location of a buried hoard. Verified, and found.", effect: { type: "gold_gain", amount: 20000 } }, + { id: "ra_e2", text: "A dormant enchantment activates and ejects two scouts. Their notes are recovered, their dignity is not.", effect: { type: "gold_loss", amount: 8000 } }, + { id: "ra_e3", text: "The archive yields a fragment that still hums with the original enchantment.", effect: { type: "material_gain", materialId: "cursed_fragment", quantity: 2 } }, + { id: "ra_e4", text: "The ancient knowledge still bleeds from the walls. Enough to be useful.", effect: { type: "essence_gain", amount: 500 } }, + ], + }, + { + id: "dragon_throne", + name: "The Dragon's Throne", + description: "The chamber the elder dragon called his own before your guild deposed him. He won't be back soon. Probably. The heat of his presence lingers in the stone.", + zoneId: "shattered_ruins", + durationSeconds: 28800, // 8h + possibleMaterials: [ + { materialId: "cursed_fragment", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "dragonscale_chip", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "dt_e1", text: "The elder dragon's hoard was larger than expected. A secondary chamber yields considerable wealth.", effect: { type: "gold_gain", amount: 40000 } }, + { id: "dt_e2", text: "The dragon left traps. Your scouts are fine. The equipment is less fine.", effect: { type: "gold_loss", amount: 15000 } }, + { id: "dt_e3", text: "A scale chip, overlooked by your previous teams, catches the light in a corner.", effect: { type: "material_gain", materialId: "dragonscale_chip", quantity: 1 } }, + { id: "dt_e4", text: "The residual draconic essence in the chamber is potent and entirely harvestable.", effect: { type: "essence_gain", amount: 800 } }, + ], + }, + + // ── Zone 3: frozen_peaks ────────────────────────────────────────────────── + { + id: "glacial_cave", + name: "The Glacial Cave", + description: "A cave carved by a glacier over thousands of years. The ice walls are so clear you can see things preserved within them from before the kingdom existed.", + zoneId: "frozen_peaks", + durationSeconds: 10800, // 3h + possibleMaterials: [ + { materialId: "glacial_ice", minQuantity: 2, maxQuantity: 5, weight: 3 }, + ], + events: [ + { id: "gc_e1", text: "A preserved cache of ancient coin, frozen for centuries, is carefully extracted.", effect: { type: "gold_gain", amount: 8000 } }, + { id: "gc_e2", text: "The ice shifted and trapped a scout briefly. Extraction was cold and expensive.", effect: { type: "gold_loss", amount: 3500 } }, + { id: "gc_e3", text: "An ice block breaks to reveal a natural hollow full of ice from an even older glacier.", effect: { type: "material_gain", materialId: "glacial_ice", quantity: 3 } }, + { id: "gc_e4", text: "Something crystalline in the cave walls draws essence from the cold itself.", effect: { type: "essence_gain", amount: 250 } }, + ], + }, + { + id: "frozen_tundra", + name: "The Frozen Tundra", + description: "Flat, white, and vast. The tundra looks featureless until you know what to look for. Under the ice, there are things that were buried with intent.", + zoneId: "frozen_peaks", + durationSeconds: 21600, // 6h + possibleMaterials: [ + { materialId: "glacial_ice", minQuantity: 3, maxQuantity: 7, weight: 3 }, + { materialId: "frost_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "ft_e1", text: "A buried shrine, untouched since before the freeze, holds its original offerings.", effect: { type: "gold_gain", amount: 18000 } }, + { id: "ft_e2", text: "A blizzard came down without warning and cost your scouts significant time and supplies.", effect: { type: "gold_loss", amount: 7000 } }, + { id: "ft_e3", text: "A frost crystal formation, exposed by recent wind erosion, is still intact.", effect: { type: "material_gain", materialId: "frost_crystal", quantity: 1 } }, + { id: "ft_e4", text: "The tundra holds old magic in its ice. Old enough to be worth distilling.", effect: { type: "essence_gain", amount: 500 } }, + ], + }, + { + id: "void_rift", + name: "The Void Rift", + description: "A tear in reality that appeared after the Void Titan's defeat, miles above the world. Something leaks through it constantly. Mostly harmless. Mostly.", + zoneId: "frozen_peaks", + durationSeconds: 32400, // 9h + possibleMaterials: [ + { materialId: "frost_crystal", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "void_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "vr_e1", text: "Something fell through the rift that clearly came from somewhere with better coinage than here.", effect: { type: "gold_gain", amount: 35000 } }, + { id: "vr_e2", text: "The rift's instability cost your scouts their equipment. They came back mostly intact.", effect: { type: "gold_loss", amount: 14000 } }, + { id: "vr_e3", text: "A void shard materialised near the rift edge and was quickly collected before it destabilised.", effect: { type: "material_gain", materialId: "void_shard", quantity: 1 } }, + { id: "vr_e4", text: "The rift leaks something that is not quite essence but distils into it cleanly.", effect: { type: "essence_gain", amount: 800 } }, + ], + }, + { + id: "summit_shrine", + name: "The Summit Shrine", + description: "At the absolute peak, a shrine nobody remembers building. The prayers still tied to its poles are in a language no scholar has identified. Offerings remain.", + zoneId: "frozen_peaks", + durationSeconds: 43200, // 12h + possibleMaterials: [ + { materialId: "frost_crystal", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "void_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "ss_e1", text: "The shrine accepts a modest offering and returns considerably more than was given. Old gods keep interesting books.", effect: { type: "gold_gain", amount: 60000 } }, + { id: "ss_e2", text: "The shrine takes offense at something. The scouts are fine. Poorer, but fine.", effect: { type: "gold_loss", amount: 22000 } }, + { id: "ss_e3", text: "A void shard rests at the shrine's base, apparently left as an offering by someone else entirely.", effect: { type: "material_gain", materialId: "void_shard", quantity: 1 } }, + { id: "ss_e4", text: "The shrine radiates an essence so dense it practically condenses on the scouts' skin.", effect: { type: "essence_gain", amount: 1500 } }, + ], + }, + + // ── Zone 4: shadow_marshes ──────────────────────────────────────────────── + { + id: "fog_hollow", + name: "The Fog Hollow", + description: "A depression in the marsh where the fog never fully lifts. Sound behaves differently here. Your scouts can hear things they probably should not.", + zoneId: "shadow_marshes", + durationSeconds: 18000, // 5h + possibleMaterials: [ + { materialId: "marsh_root", minQuantity: 2, maxQuantity: 5, weight: 3 }, + ], + events: [ + { id: "fh_e1", text: "A chest half-sunk in the mud, clearly not native to the marsh, proves worth the unpleasantness of retrieving.", effect: { type: "gold_gain", amount: 15000 } }, + { id: "fh_e2", text: "Something in the fog made the scouts spend an hour walking in circles. They returned with less than they left with.", effect: { type: "gold_loss", amount: 6000 } }, + { id: "fh_e3", text: "A stand of the toxic plants grows unusually dense in the hollow. Well-worth the careful harvest.", effect: { type: "material_gain", materialId: "marsh_root", quantity: 3 } }, + { id: "fh_e4", text: "The fog itself is distillable, in the right hands. Your alchemist has those hands.", effect: { type: "essence_gain", amount: 600 } }, + ], + }, + { + id: "dark_grotto", + name: "The Dark Grotto", + description: "A cave system beneath the marsh floor. The water drips through the ceiling in patterns that look deliberate. Nothing down here needs eyes to find you.", + zoneId: "shadow_marshes", + durationSeconds: 36000, // 10h + possibleMaterials: [ + { materialId: "marsh_root", minQuantity: 3, maxQuantity: 7, weight: 3 }, + { materialId: "shadow_essence", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "dg_e1", text: "A cache of ancient marsh-trade goods, perfectly preserved in the airless cave, sells well on the surface.", effect: { type: "gold_gain", amount: 35000 } }, + { id: "dg_e2", text: "Something that did not need eyes found your scouts anyway. They escaped. Their cargo did not.", effect: { type: "gold_loss", amount: 13000 } }, + { id: "dg_e3", text: "Shadow essence has pooled in a low point in the cave, more than usual.", effect: { type: "material_gain", materialId: "shadow_essence", quantity: 1 } }, + { id: "dg_e4", text: "The darkness in the grotto is dense enough to be harvested directly, if you know the technique.", effect: { type: "essence_gain", amount: 1000 } }, + ], + }, + { + id: "cursed_barrow", + name: "The Cursed Barrow", + description: "A burial mound. Something was interred here that should not have been — or perhaps something interred itself, which is a different and more troubling problem.", + zoneId: "shadow_marshes", + durationSeconds: 54000, // 15h + possibleMaterials: [ + { materialId: "shadow_essence", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "cursed_bone", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "cb_e1", text: "The barrow holds grave goods from three separate eras, each buried by someone who found the previous occupant's things and thought they could do better.", effect: { type: "gold_gain", amount: 70000 } }, + { id: "cb_e2", text: "The curse extends further than the survey suggested. Your scouts are fine. Their supply cache is gone.", effect: { type: "gold_loss", amount: 25000 } }, + { id: "cb_e3", text: "A cursed bone from the barrow's deepest chamber, clearly the source of the whole business.", effect: { type: "material_gain", materialId: "cursed_bone", quantity: 1 } }, + { id: "cb_e4", text: "The barrow's curse is rich in essence. Ancient and potent.", effect: { type: "essence_gain", amount: 1800 } }, + ], + }, + { + id: "marsh_depths", + name: "The Marsh Depths", + description: "The bottommost point of the Shadow Marshes, where the water is perfectly still and perfectly black. Your scouts can see the bottom. The bottom is very far down.", + zoneId: "shadow_marshes", + durationSeconds: 72000, // 20h + possibleMaterials: [ + { materialId: "shadow_essence", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "cursed_bone", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "md_e1", text: "The depths yield something that came from somewhere else entirely. It is, fortunately, convertible to coin.", effect: { type: "gold_gain", amount: 120000 } }, + { id: "md_e2", text: "Something from the depths followed your scouts partway back. The encounter was costly.", effect: { type: "gold_loss", amount: 45000 } }, + { id: "md_e3", text: "A cursed bone surfaces unprompted, as if being offered. You take it anyway.", effect: { type: "material_gain", materialId: "cursed_bone", quantity: 1 } }, + { id: "md_e4", text: "The depth-darkness is extraordinary in its potency. Your alchemist will be busy for weeks.", effect: { type: "essence_gain", amount: 3000 } }, + ], + }, + + // ── Zone 5: volcanic_depths ─────────────────────────────────────────────── + { + id: "magma_tunnel", + name: "The Magma Tunnel", + description: "A natural tunnel cut by ancient lava flows. Still warm. The walls glow faintly orange in some sections, which is either residual heat or something else.", + zoneId: "volcanic_depths", + durationSeconds: 25200, // 7h + possibleMaterials: [ + { materialId: "magma_stone", minQuantity: 2, maxQuantity: 5, weight: 3 }, + ], + events: [ + { id: "mt_e1", text: "A geothermal vent reveals a mineral deposit worth considerably more than the heat required to extract it.", effect: { type: "gold_gain", amount: 30000 } }, + { id: "mt_e2", text: "A sudden surge of superheated gas drove your scouts back and melted part of their equipment.", effect: { type: "gold_loss", amount: 12000 } }, + { id: "mt_e3", text: "The tunnel walls yield magma stones that cooled particularly slowly — higher quality than usual.", effect: { type: "material_gain", materialId: "magma_stone", quantity: 3 } }, + { id: "mt_e4", text: "The tunnel's residual magical heat is distillable if you move quickly enough.", effect: { type: "essence_gain", amount: 1000 } }, + ], + }, + { + id: "forge_chamber", + name: "The Forge Chamber", + description: "An ancient workshop space, built into the volcano by whoever the fire elementals served before they served no one. The fires here never went out.", + zoneId: "volcanic_depths", + durationSeconds: 50400, // 14h + possibleMaterials: [ + { materialId: "magma_stone", minQuantity: 3, maxQuantity: 7, weight: 3 }, + { materialId: "ember_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "fc_e1", text: "The forge chamber holds completed works, abandoned mid-project. Valuable to the right buyers.", effect: { type: "gold_gain", amount: 70000 } }, + { id: "fc_e2", text: "The elementals are more territorial than anticipated. Your scouts withdrew with minor burns and major losses.", effect: { type: "gold_loss", amount: 28000 } }, + { id: "fc_e3", text: "An ember crystal grows naturally in the forge's residual heat — considerably larger than typical.", effect: { type: "material_gain", materialId: "ember_crystal", quantity: 1 } }, + { id: "fc_e4", text: "The forge's fire-essence is ancient and extremely concentrated.", effect: { type: "essence_gain", amount: 2000 } }, + ], + }, + { + id: "fire_temple", + name: "The Fire Temple", + description: "A place of worship for entities that have never met a god but found the general idea appealing and decided to be worshipped instead. The fire elementals receive visitors here.", + zoneId: "volcanic_depths", + durationSeconds: 75600, // 21h + possibleMaterials: [ + { materialId: "ember_crystal", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "legendary_ore", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "fte_e1", text: "The temple accepts tribute and returns a blessing in the form of a significant gold windfall.", effect: { type: "gold_gain", amount: 130000 } }, + { id: "fte_e2", text: "The temple does not accept the tribute offered. The scouts return lacking both coin and dignity.", effect: { type: "gold_loss", amount: 50000 } }, + { id: "fte_e3", text: "A temple offering from a long-dead supplicant includes a piece of legendary ore, untouched.", effect: { type: "material_gain", materialId: "legendary_ore", quantity: 1 } }, + { id: "fte_e4", text: "The temple's sacred fire is extraordinary for distillation purposes.", effect: { type: "essence_gain", amount: 4000 } }, + ], + }, + { + id: "core_descent", + name: "The Core Descent", + description: "The lowest point your guild can reach — close enough to the planet's core that the rocks bleed metal and the air shimmers with heat haze that never quite resolves into anything.", + zoneId: "volcanic_depths", + durationSeconds: 100800, // 28h + possibleMaterials: [ + { materialId: "ember_crystal", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "legendary_ore", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "cd_e1", text: "At this depth, the rocks yield metals that do not exist on the surface. The sale price reflects this.", effect: { type: "gold_gain", amount: 250000 } }, + { id: "cd_e2", text: "A thermal event beyond anything survivable forced your scouts to abandon everything and run.", effect: { type: "gold_loss", amount: 90000 } }, + { id: "cd_e3", text: "The legendary ore seam here is deeper and richer than any found above.", effect: { type: "material_gain", materialId: "legendary_ore", quantity: 1 } }, + { id: "cd_e4", text: "Core-essence is unlike anything found closer to the surface. Your alchemist has no words. Just a very large smile.", effect: { type: "essence_gain", amount: 6000 } }, + ], + }, + + // ── Zone 6: astral_void ─────────────────────────────────────────────────── + { + id: "star_field", + name: "The Star Field", + description: "Open void between reality and whatever lies beyond it. Stars in various states of life and death drift past. Your scouts learn very quickly not to touch them.", + zoneId: "astral_void", + durationSeconds: 36000, // 10h + possibleMaterials: [ + { materialId: "stardust", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "sf_e1", text: "A dying star sheds its outer layers nearby. Your scouts harvest the most valuable parts.", effect: { type: "gold_gain", amount: 500000 } }, + { id: "sf_e2", text: "A stellar event of the kind that ends civilisations elsewhere merely inconvenienced your scouts and destroyed their equipment.", effect: { type: "gold_loss", amount: 200000 } }, + { id: "sf_e3", text: "A particularly fresh stardust deposit, from a star that died recently enough to still be warm.", effect: { type: "material_gain", materialId: "stardust", quantity: 4 } }, + { id: "sf_e4", text: "Void-essence is nothing like mortal-world essence but converts cleanly enough.", effect: { type: "essence_gain", amount: 10000 } }, + ], + }, + { + id: "probability_sea", + name: "The Probability Sea", + description: "A region where every possible outcome is equally real and they jostle each other for space. Your scouts exist in several states simultaneously here and find it disorienting.", + zoneId: "astral_void", + durationSeconds: 72000, // 20h + possibleMaterials: [ + { materialId: "stardust", minQuantity: 4, maxQuantity: 8, weight: 3 }, + { materialId: "astral_thread", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "ps_e1", text: "In the probability sea, your scouts found a version of events where someone paid them very well. They brought the coin back.", effect: { type: "gold_gain", amount: 1000000 } }, + { id: "ps_e2", text: "A bad probability collapsed into the scouts' timeline. The version where nothing went wrong was, unfortunately, not this one.", effect: { type: "gold_loss", amount: 400000 } }, + { id: "ps_e3", text: "An astral thread, fresh and unravelled from a probability that just resolved, is carefully harvested.", effect: { type: "material_gain", materialId: "astral_thread", quantity: 1 } }, + { id: "ps_e4", text: "Probability-essence is volatile but distils into something extraordinary.", effect: { type: "essence_gain", amount: 20000 } }, + ], + }, + { + id: "void_current", + name: "The Void Current", + description: "A river of nothing flowing through the void. It carries things from everywhere to nowhere. Some of those things are valuable, if you know how to fish from a river of nothing.", + zoneId: "astral_void", + durationSeconds: 108000, // 30h + possibleMaterials: [ + { materialId: "astral_thread", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "void_crystal", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "vc_e1", text: "The current carried through a treasury's worth of lost wealth from across time. Your scouts intercepted it.", effect: { type: "gold_gain", amount: 2000000 } }, + { id: "vc_e2", text: "The current caught two scouts and carried them downstream. They returned, eventually, with nothing.", effect: { type: "gold_loss", amount: 750000 } }, + { id: "vc_e3", text: "A void crystal, carried from somewhere, is plucked from the current before it disappears.", effect: { type: "material_gain", materialId: "void_crystal", quantity: 1 } }, + { id: "vc_e4", text: "The current distils into essence of a quality your alchemist has never encountered before.", effect: { type: "essence_gain", amount: 40000 } }, + ], + }, + { + id: "null_zenith", + name: "The Null Zenith", + description: "The highest point of the astral void, where nothing exists so thoroughly that it becomes a kind of substance. Your scouts feel, for a moment, what it is like to be absolutely alone in all of existence.", + zoneId: "astral_void", + durationSeconds: 144000, // 40h + possibleMaterials: [ + { materialId: "astral_thread", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "void_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "nz_e1", text: "The null zenith grants a moment of perfect clarity. In that moment, wealth arrives from somewhere.", effect: { type: "gold_gain", amount: 4000000 } }, + { id: "nz_e2", text: "The null zenith took something from your scouts. Possibly temporarily. The coin, certainly permanently.", effect: { type: "gold_loss", amount: 1500000 } }, + { id: "nz_e3", text: "A void crystal crystallises from the null itself, which should be impossible. It is very pretty.", effect: { type: "material_gain", materialId: "void_crystal", quantity: 1 } }, + { id: "nz_e4", text: "Null-essence is the rarest of the rare. Your alchemist faints, then recovers, then is extremely productive.", effect: { type: "essence_gain", amount: 80000 } }, + ], + }, + + // ── Zone 7: celestial_reaches ───────────────────────────────────────────── + { + id: "light_spire", + name: "The Light Spire", + description: "A tower of compressed light older than the concept of architecture. The celestial host uses it as a marker. Your guild uses it as a starting point.", + zoneId: "celestial_reaches", + durationSeconds: 43200, // 12h + possibleMaterials: [ + { materialId: "celestial_dust", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "ls_e1", text: "The spire's light reveals something valuable that was hidden precisely because it was in plain sight.", effect: { type: "gold_gain", amount: 3000000 } }, + { id: "ls_e2", text: "The celestial host noticed your scouts at the spire and expressed their disapproval economically.", effect: { type: "gold_loss", amount: 1200000 } }, + { id: "ls_e3", text: "The spire sheds celestial dust in unusual quantities today. Your scouts gather what they can.", effect: { type: "material_gain", materialId: "celestial_dust", quantity: 4 } }, + { id: "ls_e4", text: "Light-essence from the spire is extraordinarily pure.", effect: { type: "essence_gain", amount: 60000 } }, + ], + }, + { + id: "choir_hall", + name: "The Choir Hall", + description: "Where the celestial choir rehearses, continuously, for a performance that has been ongoing since before your world had an audience. The harmonics do things to objects in the vicinity.", + zoneId: "celestial_reaches", + durationSeconds: 86400, // 24h + possibleMaterials: [ + { materialId: "celestial_dust", minQuantity: 4, maxQuantity: 8, weight: 3 }, + { materialId: "divine_fragment", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "ch_e1", text: "The choir's harmonics rearranged some nearby matter into something extremely valuable.", effect: { type: "gold_gain", amount: 6000000 } }, + { id: "ch_e2", text: "The choir's harmonics are not always constructive. The scouts are intact. Their equipment is a different shape now.", effect: { type: "gold_loss", amount: 2500000 } }, + { id: "ch_e3", text: "A divine fragment, shed from the choir's apparatus, is retrieved before it dissolves.", effect: { type: "material_gain", materialId: "divine_fragment", quantity: 1 } }, + { id: "ch_e4", text: "Choir-essence resonates at a frequency that makes distillation almost automatic.", effect: { type: "essence_gain", amount: 120000 } }, + ], + }, + { + id: "divine_court", + name: "The Divine Court", + description: "Where the celestial host adjudicates disputes that have been ongoing since before your sun was lit. The proceedings are extremely formal. Interrupting them is inadvisable.", + zoneId: "celestial_reaches", + durationSeconds: 129600, // 36h + possibleMaterials: [ + { materialId: "divine_fragment", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "choir_shard", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "dc_e1", text: "A case was decided in your guild's favour by technicality. The awarded wealth is considerable.", effect: { type: "gold_gain", amount: 12000000 } }, + { id: "dc_e2", text: "A case was decided against your guild by technicality. The levied fine is considerable.", effect: { type: "gold_loss", amount: 5000000 } }, + { id: "dc_e3", text: "A choir shard falls from the court's apparatus during a particularly resonant moment of judgment.", effect: { type: "material_gain", materialId: "choir_shard", quantity: 1 } }, + { id: "dc_e4", text: "Divine court essence is regulated. Yours is unregulated. The difference is significant.", effect: { type: "essence_gain", amount: 250000 } }, + ], + }, + { + id: "celestial_vault", + name: "The Celestial Vault", + description: "Where the celestial host stores things they consider too valuable to use and too important to discard. Your guild has different ideas about what 'valuable' means.", + zoneId: "celestial_reaches", + durationSeconds: 172800, // 48h + possibleMaterials: [ + { materialId: "divine_fragment", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "choir_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "cv_e1", text: "The vault contains a complete set of something the celestials forgot about. The set is worth a considerable fortune.", effect: { type: "gold_gain", amount: 25000000 } }, + { id: "cv_e2", text: "The vault's security noticed your scouts on the way out. The items were recovered. The scouts were charged a fine.", effect: { type: "gold_loss", amount: 10000000 } }, + { id: "cv_e3", text: "A choir shard in the vault's collection, misidentified and misfiled. It comes willingly.", effect: { type: "material_gain", materialId: "choir_shard", quantity: 1 } }, + { id: "cv_e4", text: "Vault-essence is the highest quality your alchemist has seen. She writes three papers about it immediately.", effect: { type: "essence_gain", amount: 500000 } }, + ], + }, + + // ── Zone 8: abyssal_trench ──────────────────────────────────────────────── + { + id: "trench_entrance", + name: "The Trench Entrance", + description: "The lip of the trench, where the shelf drops away into depths that swallow light entirely. Your scouts can hear something breathing, very slowly, from far below.", + zoneId: "abyssal_trench", + durationSeconds: 50400, // 14h + possibleMaterials: [ + { materialId: "trench_coral", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "te_e1", text: "The shelf is littered with things that fell in and were preserved by the pressure before being pushed back up.", effect: { type: "gold_gain", amount: 8000000 } }, + { id: "te_e2", text: "Something reached up from below. Your scouts are fine. Their equipment is at the bottom of the trench.", effect: { type: "gold_loss", amount: 3000000 } }, + { id: "te_e3", text: "The trench coral near the entrance is more abundant than the survey suggested.", effect: { type: "material_gain", materialId: "trench_coral", quantity: 4 } }, + { id: "te_e4", text: "Trench-essence is crushingly dense. Very difficult to handle. Extremely rewarding.", effect: { type: "essence_gain", amount: 150000 } }, + ], + }, + { + id: "deep_current", + name: "The Deep Current", + description: "An underwater river at a depth that should be impossible to survive. Your scouts have learned, by necessity, to survive it anyway.", + zoneId: "abyssal_trench", + durationSeconds: 100800, // 28h + possibleMaterials: [ + { materialId: "trench_coral", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "pressure_gem", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "dep_e1", text: "The current carries deposits from further and deeper than your scouts can navigate. Today it brought them something valuable.", effect: { type: "gold_gain", amount: 18000000 } }, + { id: "dep_e2", text: "The current is faster today than yesterday. Your scouts arrived downstream, missing considerable cargo.", effect: { type: "gold_loss", amount: 7000000 } }, + { id: "dep_e3", text: "A pressure gem tumbles along the current floor, collecting more pressure as it goes. Your scouts intercept it.", effect: { type: "material_gain", materialId: "pressure_gem", quantity: 1 } }, + { id: "dep_e4", text: "Current-essence is dense and strange. Your alchemist has questions. The essence has no answers but lots of potential.", effect: { type: "essence_gain", amount: 350000 } }, + ], + }, + { + id: "sunless_chamber", + name: "The Sunless Chamber", + description: "A space at the bottom of the trench so far from light that light has no meaning here. Something has been in this chamber for so long it no longer needs to breathe.", + zoneId: "abyssal_trench", + durationSeconds: 151200, // 42h + possibleMaterials: [ + { materialId: "pressure_gem", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "ancient_tooth", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "sc_e1", text: "The chamber holds things that have been here since before there was a bottom to the sea. They are worth a great deal.", effect: { type: "gold_gain", amount: 40000000 } }, + { id: "sc_e2", text: "Something in the chamber objects to being disturbed. It does not give chase. It simply ensures your scouts leave lighter.", effect: { type: "gold_loss", amount: 15000000 } }, + { id: "sc_e3", text: "An ancient tooth, clearly from the chamber's oldest resident, is found near the entrance.", effect: { type: "material_gain", materialId: "ancient_tooth", quantity: 1 } }, + { id: "sc_e4", text: "Sunless-essence is unlike anything from above. It seems to draw light into itself. Remarkable.", effect: { type: "essence_gain", amount: 700000 } }, + ], + }, + { + id: "the_waiting_place", + name: "The Waiting Place", + description: "The absolute bottom of the trench. Something is here. It has been here since before your world was made. It is, today, patient. Your scouts are not sure this is always the case.", + zoneId: "abyssal_trench", + durationSeconds: 201600, // 56h + possibleMaterials: [ + { materialId: "pressure_gem", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "ancient_tooth", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "wp_e1", text: "Whatever waits here acknowledges your guild's presence with something that translates, approximately, to wealth.", effect: { type: "gold_gain", amount: 80000000 } }, + { id: "wp_e2", text: "Whatever waits here acknowledges your guild's presence in a way that translates, approximately, to a fine.", effect: { type: "gold_loss", amount: 30000000 } }, + { id: "wp_e3", text: "An ancient tooth surfaces, offered. Taking it feels like something significant. It probably is.", effect: { type: "material_gain", materialId: "ancient_tooth", quantity: 1 } }, + { id: "wp_e4", text: "Waiting-essence is old beyond measure. Your alchemist refuses to speculate about what it once was.", effect: { type: "essence_gain", amount: 1500000 } }, + ], + }, + + // ── Zone 9: infernal_court ──────────────────────────────────────────────── + { + id: "demon_market", + name: "The Demon Market", + description: "An open-air market in the court's outer districts. The vendors sell things that were not legally obtained, in exchange for things that should not legally exist.", + zoneId: "infernal_court", + durationSeconds: 57600, // 16h + possibleMaterials: [ + { materialId: "brimstone_flake", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "dm_e1", text: "Your scouts negotiated a trade that was, by infernal standards, entirely fair. By mortal standards, extraordinary.", effect: { type: "gold_gain", amount: 20000000 } }, + { id: "dm_e2", text: "A market vendor offered a deal that seemed reasonable. Your scouts are learning. Too slowly.", effect: { type: "gold_loss", amount: 8000000 } }, + { id: "dm_e3", text: "A vendor selling brimstone flakes has surplus today. Your scouts negotiate bulk pricing.", effect: { type: "material_gain", materialId: "brimstone_flake", quantity: 4 } }, + { id: "dm_e4", text: "Market-essence carries the weight of every deal ever made here. There have been many deals.", effect: { type: "essence_gain", amount: 400000 } }, + ], + }, + { + id: "torment_hall", + name: "The Torment Hall", + description: "Where the court processes those who lost their cases. Your scouts move through it quickly and look at nothing. They still hear everything.", + zoneId: "infernal_court", + durationSeconds: 115200, // 32h + possibleMaterials: [ + { materialId: "brimstone_flake", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "demon_ichor", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "th_e1", text: "A case file, misfiled in the hall, contains a writ authorising a wealth disbursement. Your guild is not the named recipient, but the court's filing system is poor.", effect: { type: "gold_gain", amount: 45000000 } }, + { id: "th_e2", text: "A hall official noticed your scouts and extracted what he considered an appropriate visitation fee.", effect: { type: "gold_loss", amount: 18000000 } }, + { id: "th_e3", text: "Demon ichor pools on the hall floor. Your alchemist's instructions were very specific about this.", effect: { type: "material_gain", materialId: "demon_ichor", quantity: 1 } }, + { id: "th_e4", text: "Torment-essence is potent in the way that only very old suffering can be. Your alchemist handles it carefully.", effect: { type: "essence_gain", amount: 900000 } }, + ], + }, + { + id: "soul_forge", + name: "The Soul Forge", + description: "The court's industrial district, where deals are processed and the residue of completed contracts is extracted. The machinery runs on something the court considers renewable.", + zoneId: "infernal_court", + durationSeconds: 172800, // 48h + possibleMaterials: [ + { materialId: "demon_ichor", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "soul_residue", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "sof_e1", text: "The forge's overflow system is backed up. Your scouts help clear it for a significant consideration.", effect: { type: "gold_gain", amount: 90000000 } }, + { id: "sof_e2", text: "The forge's output included your scouts' equipment in the residue stream. Recovery was partial.", effect: { type: "gold_loss", amount: 35000000 } }, + { id: "sof_e3", text: "The forge produces soul residue as a byproduct. Today's output is substantial.", effect: { type: "material_gain", materialId: "soul_residue", quantity: 1 } }, + { id: "sof_e4", text: "Forge-essence is extremely concentrated. Your alchemist insists on double-walled containers.", effect: { type: "essence_gain", amount: 2000000 } }, + ], + }, + { + id: "lords_chamber", + name: "The Lords' Chamber", + description: "The inner sanctum of the infernal court, where the demon lords make decisions that echo across aeons. Your guild should not be here. Your guild is here anyway.", + zoneId: "infernal_court", + durationSeconds: 230400, // 64h + possibleMaterials: [ + { materialId: "demon_ichor", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "soul_residue", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "lc_e1", text: "A lord, impressed by the audacity of your guild's presence, made an offer. Your scouts accepted and are remarkably wealthy.", effect: { type: "gold_gain", amount: 180000000 } }, + { id: "lc_e2", text: "A lord, irritated by the audacity of your guild's presence, made a different kind of offer. Your scouts declined but paid a fee.", effect: { type: "gold_loss", amount: 70000000 } }, + { id: "lc_e3", text: "Soul residue collects in the chamber's corners, unattended. Your scouts are very fast.", effect: { type: "material_gain", materialId: "soul_residue", quantity: 1 } }, + { id: "lc_e4", text: "Lords' chamber essence is the most potent infernal essence your alchemist has ever worked with. She requests a raise.", effect: { type: "essence_gain", amount: 4000000 } }, + ], + }, + + // ── Zone 10: crystalline_spire ──────────────────────────────────────────── + { + id: "facet_approach", + name: "The Facet Approach", + description: "The outer surface of the spire, where thousands of crystal facets reflect realities that are not the one you arrived in. Your scouts learn to focus on the ground in front of them.", + zoneId: "crystalline_spire", + durationSeconds: 64800, // 18h + possibleMaterials: [ + { materialId: "prism_dust", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "fa_e1", text: "One facet shows a version of events where someone left a treasure here. Checking: yes. It is here.", effect: { type: "gold_gain", amount: 60000000 } }, + { id: "fa_e2", text: "A facet showed your scouts a path. The path led somewhere considerably less profitable than expected.", effect: { type: "gold_loss", amount: 24000000 } }, + { id: "fa_e3", text: "A facet chips free and sheds prism dust in exceptional quantity.", effect: { type: "material_gain", materialId: "prism_dust", quantity: 4 } }, + { id: "fa_e4", text: "Facet-essence refracts light into something your alchemist can work with. It's beautiful and extremely useful.", effect: { type: "essence_gain", amount: 1200000 } }, + ], + }, + { + id: "calculation_chamber", + name: "The Calculation Chamber", + description: "A room inside the spire where the intelligence runs its oldest and most complex calculations. The numbers on the walls change too fast to read. The calculations are always correct.", + zoneId: "crystalline_spire", + durationSeconds: 129600, // 36h + possibleMaterials: [ + { materialId: "prism_dust", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "calculation_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "cc_e1", text: "The calculations briefly solved for your guild's maximum possible wealth. They were not entirely wrong.", effect: { type: "gold_gain", amount: 130000000 } }, + { id: "cc_e2", text: "The calculations included your scouts in an equation. The solution involved them being considerably poorer.", effect: { type: "gold_loss", amount: 50000000 } }, + { id: "cc_e3", text: "A calculation shard breaks free when a particularly complex proof is resolved.", effect: { type: "material_gain", materialId: "calculation_shard", quantity: 1 } }, + { id: "cc_e4", text: "Calculation-essence is mathematically pure. Your alchemist stops mid-process to write a proof about it.", effect: { type: "essence_gain", amount: 2500000 } }, + ], + }, + { + id: "mirror_hall", + name: "The Mirror Hall", + description: "A corridor of perfect mirrors that show not reflections but what might have been. Your scouts avoid eye contact with their alternates. The alternates do not always extend the same courtesy.", + zoneId: "crystalline_spire", + durationSeconds: 194400, // 54h + possibleMaterials: [ + { materialId: "calculation_shard", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "possibility_crystal", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "mh_e1", text: "An alternate timeline's version of this expedition was more successful. Your scouts found where they stored it.", effect: { type: "gold_gain", amount: 270000000 } }, + { id: "mh_e2", text: "An alternate scout followed your team out and made off with a significant portion of the haul.", effect: { type: "gold_loss", amount: 105000000 } }, + { id: "mh_e3", text: "A possibility crystal forms at the junction of two mirrors showing the same impossible future.", effect: { type: "material_gain", materialId: "possibility_crystal", quantity: 1 } }, + { id: "mh_e4", text: "Mirror-essence contains reflections of all possible essences simultaneously. Your alchemist collapses the wave function very carefully.", effect: { type: "essence_gain", amount: 5500000 } }, + ], + }, + { + id: "core_access", + name: "The Core Access", + description: "The deepest point of the spire, where the intelligence's primary substrate runs continuously. The hum of calculation is felt in the bones. Numbers that have never been numbers drift past.", + zoneId: "crystalline_spire", + durationSeconds: 259200, // 72h + possibleMaterials: [ + { materialId: "calculation_shard", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "possibility_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "ca_e1", text: "The intelligence notices your guild and — apparently — approves. The approval is expressed financially.", effect: { type: "gold_gain", amount: 550000000 } }, + { id: "ca_e2", text: "The intelligence notices your guild and expresses its calculations as a fine for unauthorised access.", effect: { type: "gold_loss", amount: 210000000 } }, + { id: "ca_e3", text: "A possibility crystal from the core's deepest level — containing futures that only this intelligence has computed.", effect: { type: "material_gain", materialId: "possibility_crystal", quantity: 1 } }, + { id: "ca_e4", text: "Core-essence is computational in a way that has no analogue in mortal chemistry. Your alchemist needs new words.", effect: { type: "essence_gain", amount: 11000000 } }, + ], + }, + + // ── Zone 11: void_sanctum ───────────────────────────────────────────────── + { + id: "threshold", + name: "The Threshold", + description: "The entrance to the void sanctum, where the rules of existence become suggestions. Your scouts describe the crossing as like stepping sideways and arriving somewhere that was always there.", + zoneId: "void_sanctum", + durationSeconds: 72000, // 20h + possibleMaterials: [ + { materialId: "null_matter", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "thr_e1", text: "The threshold allows things to cross that should not exist in normal space. Some of those things are valuable.", effect: { type: "gold_gain", amount: 200000000 } }, + { id: "thr_e2", text: "The threshold is less stable today. Your scouts crossed it but the equipment did not follow completely.", effect: { type: "gold_loss", amount: 80000000 } }, + { id: "thr_e3", text: "Null matter accumulates at the threshold where something becomes nothing. There is quite a lot of it today.", effect: { type: "material_gain", materialId: "null_matter", quantity: 4 } }, + { id: "thr_e4", text: "Threshold-essence is transitional — partway between being and not-being. Your alchemist finds this philosophically troubling and practically wonderful.", effect: { type: "essence_gain", amount: 4000000 } }, + ], + }, + { + id: "inner_silence", + name: "The Inner Silence", + description: "A place inside the sanctum where everything is perfectly quiet because nothing exists to make noise. Your scouts can hear their own thoughts very clearly here. Some of them find this unsettling.", + zoneId: "void_sanctum", + durationSeconds: 144000, // 40h + possibleMaterials: [ + { materialId: "null_matter", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "resonance_fragment", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "is_e1", text: "In the silence, your scouts heard the sound of wealth. Following it was straightforward.", effect: { type: "gold_gain", amount: 420000000 } }, + { id: "is_e2", text: "In the silence, your scouts heard their equipment being quietly redistributed. The sound of nothing taking things.", effect: { type: "gold_loss", amount: 160000000 } }, + { id: "is_e3", text: "A resonance fragment reverberates in the silence, audible when nothing else is.", effect: { type: "material_gain", materialId: "resonance_fragment", quantity: 1 } }, + { id: "is_e4", text: "Silence-essence is collected by having nothing absorb it. Your alchemist uses a specially prepared container.", effect: { type: "essence_gain", amount: 8000000 } }, + ], + }, + { + id: "resonance_chamber", + name: "The Resonance Chamber", + description: "A space inside the sanctum where something is calling out, continuously, to something that has not yet answered. The call is beautiful and deeply wrong.", + zoneId: "void_sanctum", + durationSeconds: 216000, // 60h + possibleMaterials: [ + { materialId: "resonance_fragment", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "sanctum_core", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "rc_e1", text: "The call briefly aligned with something profitable. Your scouts followed the alignment quickly.", effect: { type: "gold_gain", amount: 900000000 } }, + { id: "rc_e2", text: "The call briefly aligned with something expensive. The invoice was delivered before your scouts could leave.", effect: { type: "gold_loss", amount: 350000000 } }, + { id: "rc_e3", text: "A sanctum core materialises at the chamber's focal point, called into being by the resonance itself.", effect: { type: "material_gain", materialId: "sanctum_core", quantity: 1 } }, + { id: "rc_e4", text: "Resonance-essence carries the call within it. Your alchemist is very careful not to answer it.", effect: { type: "essence_gain", amount: 18000000 } }, + ], + }, + { + id: "sanctum_heart", + name: "The Sanctum Heart", + description: "The source of the call. Something here has been reaching out for so long it no longer remembers what it is reaching toward. Your guild's arrival is, perhaps, an answer.", + zoneId: "void_sanctum", + durationSeconds: 288000, // 80h + possibleMaterials: [ + { materialId: "resonance_fragment", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "sanctum_core", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "sh_e1", text: "The heart recognises your guild as an answer of sorts and expresses gratitude in the most comprehensible way available to it: wealth.", effect: { type: "gold_gain", amount: 1800000000 } }, + { id: "sh_e2", text: "The heart mistakes your guild for something it has been dreading. The misunderstanding is expensive.", effect: { type: "gold_loss", amount: 700000000 } }, + { id: "sh_e3", text: "A sanctum core from the heart itself — the source of everything the sanctum is and does.", effect: { type: "material_gain", materialId: "sanctum_core", quantity: 1 } }, + { id: "sh_e4", text: "Heart-essence from the void sanctum. Your alchemist sits very still for a long time before beginning.", effect: { type: "essence_gain", amount: 36000000 } }, + ], + }, + + // ── Zone 12: eternal_throne ─────────────────────────────────────────────── + { + id: "throne_approach", + name: "The Throne Approach", + description: "The long road to the eternal throne. Countless beings have walked it, seeking audience, seeking power, seeking something the throne has always already decided about them.", + zoneId: "eternal_throne", + durationSeconds: 79200, // 22h + possibleMaterials: [ + { materialId: "throne_dust", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "ta_e1", text: "A pilgrim left their worldly wealth at the approach before continuing. They did not return for it. Your scouts did.", effect: { type: "gold_gain", amount: 700000000 } }, + { id: "ta_e2", text: "A customs toll, ancient and automatically enforced, extracted a fee from your scouts. The mechanism was unavoidable.", effect: { type: "gold_loss", amount: 280000000 } }, + { id: "ta_e3", text: "The throne dust on the approach is deep and old. Your scouts collect the finest layer from the top.", effect: { type: "material_gain", materialId: "throne_dust", quantity: 4 } }, + { id: "ta_e4", text: "Approach-essence carries the weight of every being who has ever walked this road. There have been very many.", effect: { type: "essence_gain", amount: 14000000 } }, + ], + }, + { + id: "dominion_hall", + name: "The Dominion Hall", + description: "The ante-chamber of absolute power. Records are kept here of everything that has ever been ruled and everything that has ever been lost. The records go back further than memory.", + zoneId: "eternal_throne", + durationSeconds: 158400, // 44h + possibleMaterials: [ + { materialId: "throne_dust", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "crown_fragment", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "doh_e1", text: "The hall's records include an unclaimed inheritance from a petitioner who never arrived. Your guild files the appropriate claim.", effect: { type: "gold_gain", amount: 1400000000 } }, + { id: "doh_e2", text: "The hall discovered an outstanding tax from a guild registered centuries ago with a similar name. Enforcement was automatic.", effect: { type: "gold_loss", amount: 550000000 } }, + { id: "doh_e3", text: "A crown fragment from a petition that was decided eons ago, still attached to its filing.", effect: { type: "material_gain", materialId: "crown_fragment", quantity: 1 } }, + { id: "doh_e4", text: "Dominion-essence is the essence of authority made distillable. Your alchemist treats it with appropriate respect.", effect: { type: "essence_gain", amount: 28000000 } }, + ], + }, + { + id: "eternity_vault", + name: "The Eternity Vault", + description: "Where things are stored that have nowhere else to go. Objects of power that cannot be used, secrets that cannot be shared, and wealth that belongs to entities that stopped existing before your world was born.", + zoneId: "eternal_throne", + durationSeconds: 237600, // 66h + possibleMaterials: [ + { materialId: "crown_fragment", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "eternity_splinter", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "ev_e1", text: "The vault contains a misfiled deposit from an empire that is no longer around to claim it. Your guild is.", effect: { type: "gold_gain", amount: 3000000000 } }, + { id: "ev_e2", text: "A vault security system, last updated before your species evolved, extracted an access fee your scouts had no choice but to pay.", effect: { type: "gold_loss", amount: 1200000000 } }, + { id: "ev_e3", text: "An eternity splinter from a filing that predates the current occupant of the throne. Unclaimed.", effect: { type: "material_gain", materialId: "eternity_splinter", quantity: 1 } }, + { id: "ev_e4", text: "Vault-essence is so old it has crystallised into something that barely resembles essence anymore. Your alchemist is delighted.", effect: { type: "essence_gain", amount: 60000000 } }, + ], + }, + { + id: "the_seat", + name: "The Seat", + description: "The eternal throne itself. Whoever sits here has sat here since the beginning. They observe your guild's presence with neither surprise nor emotion. They have been expecting you. They have been expecting everyone.", + zoneId: "eternal_throne", + durationSeconds: 316800, // 88h + possibleMaterials: [ + { materialId: "crown_fragment", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "eternity_splinter", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "ts_e1", text: "The occupant of the throne acknowledges your guild's petition. The acknowledgment comes with a substantial material consideration.", effect: { type: "gold_gain", amount: 6000000000 } }, + { id: "ts_e2", text: "The occupant of the throne acknowledged your guild's presence with a tithe. Ancient thrones collect ancient tithes.", effect: { type: "gold_loss", amount: 2300000000 } }, + { id: "ts_e3", text: "An eternity splinter from the throne's arm, offered without ceremony. You accept without ceremony.", effect: { type: "material_gain", materialId: "eternity_splinter", quantity: 1 } }, + { id: "ts_e4", text: "Throne-essence contains everything that has ever been decided from this seat. Your alchemist is overwhelmed. In a good way.", effect: { type: "essence_gain", amount: 120000000 } }, + ], + }, + + // ── Zone 13: primordial_chaos ───────────────────────────────────────────── + { + id: "creation_storm", + name: "The Creation Storm", + description: "A permanent storm at the edge of the chaos zone where things are constantly being made and unmade simultaneously. Your scouts move through it quickly and try not to look at what they might become.", + zoneId: "primordial_chaos", + durationSeconds: 86400, // 24h + possibleMaterials: [ + { materialId: "chaos_fragment", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "cs_e1", text: "The storm created something valuable during your scouts' passage. They recognised what it was and took it.", effect: { type: "gold_gain", amount: 2000000000 } }, + { id: "cs_e2", text: "The storm unmade something your scouts were carrying. The loss was structural, not merely economic.", effect: { type: "gold_loss", amount: 800000000 } }, + { id: "cs_e3", text: "A chaos fragment solidifies during a moment of relative stability in the storm.", effect: { type: "material_gain", materialId: "chaos_fragment", quantity: 4 } }, + { id: "cs_e4", text: "Creation-storm essence is freshly made existence. Your alchemist handles it like it is alive. It might be.", effect: { type: "essence_gain", amount: 40000000 } }, + ], + }, + { + id: "unmaking_sea", + name: "The Unmaking Sea", + description: "A vast ocean of something that is exactly the opposite of matter. Your scouts cross it by not thinking too hard about what they are standing on.", + zoneId: "primordial_chaos", + durationSeconds: 172800, // 48h + possibleMaterials: [ + { materialId: "chaos_fragment", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "creation_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "us_e1", text: "In the unmaking sea, something was unmade that revealed what was underneath it, which was quite a lot of gold.", effect: { type: "gold_gain", amount: 4000000000 } }, + { id: "us_e2", text: "The sea unmade a section of the expedition's equipment. The scouts swam through nothing to retrieve nothing.", effect: { type: "gold_loss", amount: 1600000000 } }, + { id: "us_e3", text: "A creation shard surfaces from the sea, the only solid thing within a considerable radius.", effect: { type: "material_gain", materialId: "creation_shard", quantity: 1 } }, + { id: "us_e4", text: "Unmaking-essence is paradoxically the most generative substance your alchemist has worked with.", effect: { type: "essence_gain", amount: 80000000 } }, + ], + }, + { + id: "probability_void", + name: "The Probability Void", + description: "A space where all possible outcomes already happened and none of them mattered. Your scouts find this philosophically challenging and practically navigable.", + zoneId: "primordial_chaos", + durationSeconds: 259200, // 72h + possibleMaterials: [ + { materialId: "creation_shard", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "primordial_essence", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "pv_e1", text: "One of the possible outcomes that already happened involved significant wealth for your guild. They located it.", effect: { type: "gold_gain", amount: 8000000000 } }, + { id: "pv_e2", text: "One of the possible outcomes that already happened involved a significant loss. It applied retroactively.", effect: { type: "gold_loss", amount: 3200000000 } }, + { id: "pv_e3", text: "A primordial essence crystallises at the point where all probabilities converge.", effect: { type: "material_gain", materialId: "primordial_essence", quantity: 1 } }, + { id: "pv_e4", text: "Probability-void essence contains every possible essence simultaneously. Your alchemist collapses it into the best one.", effect: { type: "essence_gain", amount: 160000000 } }, + ], + }, + { + id: "chaos_core", + name: "The Chaos Core", + description: "The centre of all primordial chaos. Everything is here and nothing is here and both statements are entirely accurate. Your scouts report the experience as indescribable, then describe it for three hours.", + zoneId: "primordial_chaos", + durationSeconds: 345600, // 96h + possibleMaterials: [ + { materialId: "creation_shard", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "primordial_essence", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "chc_e1", text: "The chaos core created something specifically for your guild. It was valuable beyond measure. Your scouts measured it anyway: very.", effect: { type: "gold_gain", amount: 16000000000 } }, + { id: "chc_e2", text: "The chaos core unmade something specifically belonging to your guild. The loss is immeasurable. Your accountant measures it anyway.", effect: { type: "gold_loss", amount: 6500000000 } }, + { id: "chc_e3", text: "A primordial essence directly from the core — as close to the original substance of creation as anything can be.", effect: { type: "material_gain", materialId: "primordial_essence", quantity: 1 } }, + { id: "chc_e4", text: "Core-chaos essence is literally the substance from which everything was made. Your alchemist sits in silence for a very long time.", effect: { type: "essence_gain", amount: 320000000 } }, + ], + }, + + // ── Zone 14: infinite_expanse ───────────────────────────────────────────── + { + id: "first_horizon", + name: "The First Horizon", + description: "The first horizon you reach in the infinite expanse, which looks exactly like the starting point from behind but is provably, mathematically, somewhere else. Your scouts are sceptical but cannot argue with the math.", + zoneId: "infinite_expanse", + durationSeconds: 93600, // 26h + possibleMaterials: [ + { materialId: "expanse_dust", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "fh_e1", text: "The horizon concealed something behind it that, from this side, proves to be worth considerably more than the journey.", effect: { type: "gold_gain", amount: 6000000000 } }, + { id: "fh_e2", text: "The horizon reflects something back at your scouts that arrived before they did, specifically to collect from them.", effect: { type: "gold_loss", amount: 2400000000 } }, + { id: "fh_e3", text: "Expanse dust accumulates at horizon lines where distance compresses. A good day to harvest.", effect: { type: "material_gain", materialId: "expanse_dust", quantity: 4 } }, + { id: "fh_e4", text: "Horizon-essence is the essence of boundary — of here and not-here simultaneously. Your alchemist finds it revelatory.", effect: { type: "essence_gain", amount: 120000000 } }, + ], + }, + { + id: "middle_nowhere", + name: "The Middle of Nowhere", + description: "There is no centre of the infinite expanse. This is the centre of the infinite expanse. Both things are true. Your scouts have stopped asking questions and started collecting samples.", + zoneId: "infinite_expanse", + durationSeconds: 187200, // 52h + possibleMaterials: [ + { materialId: "expanse_dust", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "distance_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "mn_e1", text: "The middle of nowhere contains something that was lost so thoroughly it became the definition of lost. Your scouts found it.", effect: { type: "gold_gain", amount: 12000000000 } }, + { id: "mn_e2", text: "Your scouts lost something so thoroughly at the middle of nowhere that even the expanse could not locate it.", effect: { type: "gold_loss", amount: 4800000000 } }, + { id: "mn_e3", text: "A distance crystal at the exact geometric impossibility of the expanse's centre.", effect: { type: "material_gain", materialId: "distance_crystal", quantity: 1 } }, + { id: "mn_e4", text: "Nowhere-essence is the concentrated experience of there being nothing here. Your alchemist finds it unexpectedly full.", effect: { type: "essence_gain", amount: 240000000 } }, + ], + }, + { + id: "edge_approach", + name: "The Edge Approach", + description: "The road toward the edge that the expanse does not have. Your scouts know it does not exist. They are getting closer to it anyway.", + zoneId: "infinite_expanse", + durationSeconds: 280800, // 78h + possibleMaterials: [ + { materialId: "distance_crystal", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "infinity_shard", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "ea_e1", text: "Near the edge that does not exist, things that should not exist are more concentrated. Including wealth.", effect: { type: "gold_gain", amount: 25000000000 } }, + { id: "ea_e2", text: "Something at the approach collected a toll for proximity to an edge that does not exist. The toll was real.", effect: { type: "gold_loss", amount: 10000000000 } }, + { id: "ea_e3", text: "An infinity shard from where the edge would be, if the edge were real. It is very much real.", effect: { type: "material_gain", materialId: "infinity_shard", quantity: 1 } }, + { id: "ea_e4", text: "Edge-approach essence carries the feeling of being almost at something. It is very motivating.", effect: { type: "essence_gain", amount: 500000000 } }, + ], + }, + { + id: "the_furthest", + name: "The Furthest", + description: "As far as any being has ever gone in the infinite expanse. Your scouts hold this record now. They are not entirely sure whether to be proud or frightened.", + zoneId: "infinite_expanse", + durationSeconds: 374400, // 104h + possibleMaterials: [ + { materialId: "distance_crystal", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "infinity_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "tf_e1", text: "The furthest point holds the furthest things. The furthest things are very valuable.", effect: { type: "gold_gain", amount: 50000000000 } }, + { id: "tf_e2", text: "Getting home from the furthest point is expensive. Distance is not your guild's friend today.", effect: { type: "gold_loss", amount: 20000000000 } }, + { id: "tf_e3", text: "An infinity shard from the furthest any expedition has gone — carrying more distance than should fit in it.", effect: { type: "material_gain", materialId: "infinity_shard", quantity: 1 } }, + { id: "tf_e4", text: "Furthest-essence is the essence of absolute distance. Your alchemist works on it from a very long way away, symbolically.", effect: { type: "essence_gain", amount: 1000000000 } }, + ], + }, + + // ── Zone 15: reality_forge ──────────────────────────────────────────────── + { + id: "workshop_entrance", + name: "The Workshop Entrance", + description: "The outer area of the reality forge, where the overflow of unrealised realities pools and cools. Things that never quite existed are everywhere here, and some of them are extremely useful.", + zoneId: "reality_forge", + durationSeconds: 100800, // 28h + possibleMaterials: [ + { materialId: "forge_ash", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "we_e1", text: "The overflow pool contains rejected realities with useful properties. Your scouts extract what they can.", effect: { type: "gold_gain", amount: 20000000000 } }, + { id: "we_e2", text: "A rejected reality became briefly real enough to take something from your scouts before being rejected again.", effect: { type: "gold_loss", amount: 8000000000 } }, + { id: "we_e3", text: "Forge ash from this batch contains particularly dense unrealised potential.", effect: { type: "material_gain", materialId: "forge_ash", quantity: 4 } }, + { id: "we_e4", text: "Workshop-entrance essence is the overflow of creation — the part that did not make it into anything.", effect: { type: "essence_gain", amount: 400000000 } }, + ], + }, + { + id: "creation_floor", + name: "The Creation Floor", + description: "Where realities are assembled from the raw components of existence. The work here is continuous and has been going on since before your universe was queued.", + zoneId: "reality_forge", + durationSeconds: 201600, // 56h + possibleMaterials: [ + { materialId: "forge_ash", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "creation_tool", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "cfl_e1", text: "A reality in production is briefly assigned to your guild's specifications. The payment for this is substantial.", effect: { type: "gold_gain", amount: 40000000000 } }, + { id: "cfl_e2", text: "A reality in production incorrectly assigned a debt to your guild. The forge's billing department is centuries behind.", effect: { type: "gold_loss", amount: 16000000000 } }, + { id: "cfl_e3", text: "A worn creation tool, left by a worker who has not returned to claim it. Still perfectly functional.", effect: { type: "material_gain", materialId: "creation_tool", quantity: 1 } }, + { id: "cfl_e4", text: "Floor-essence is the breath of ongoing creation. Your alchemist breathes it carefully and makes extensive notes.", effect: { type: "essence_gain", amount: 800000000 } }, + ], + }, + { + id: "master_forge", + name: "The Master Forge", + description: "The primary forging station, where major realities are hammered into their final shape. The hammers are larger than planets. The anvil has never been named because no one has ever successfully described it.", + zoneId: "reality_forge", + durationSeconds: 302400, // 84h + possibleMaterials: [ + { materialId: "creation_tool", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "reality_shard", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "mf_e1", text: "A forging commission was misfiled and your guild was listed as the recipient. The commission is worth immeasurably more than usual.", effect: { type: "gold_gain", amount: 80000000000 } }, + { id: "mf_e2", text: "The forge's scheduling error assigned your guild as collateral for a major commission. The fee was astronomical.", effect: { type: "gold_loss", amount: 32000000000 } }, + { id: "mf_e3", text: "A reality shard, rejected as below the master forge's standards. By any other measure: extraordinary.", effect: { type: "material_gain", materialId: "reality_shard", quantity: 1 } }, + { id: "mf_e4", text: "Master-forge essence carries the heat and purpose of reality-making. Your alchemist handles it with forge-grade equipment.", effect: { type: "essence_gain", amount: 1600000000 } }, + ], + }, + { + id: "forge_core", + name: "The Forge Core", + description: "The energy source that powers the entire reality forge. It has been running since before time was a meaningful concept. What powers it is not a question that has been answered by anyone who came here to ask it.", + zoneId: "reality_forge", + durationSeconds: 403200, // 112h + possibleMaterials: [ + { materialId: "creation_tool", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "reality_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "fc2_e1", text: "The forge core outputs something every few billion years. Today it outputted something. Your scouts were there.", effect: { type: "gold_gain", amount: 160000000000 } }, + { id: "fc2_e2", text: "The forge core requires a tithe from anything that approaches it. It always has. The amount is non-negotiable.", effect: { type: "gold_loss", amount: 65000000000 } }, + { id: "fc2_e3", text: "A reality shard from the forge core itself — something that could have been a universe if the settings had been slightly different.", effect: { type: "material_gain", materialId: "reality_shard", quantity: 1 } }, + { id: "fc2_e4", text: "Core-forge essence is the power of creation itself. Your alchemist declines to speculate about what it means.", effect: { type: "essence_gain", amount: 3200000000 } }, + ], + }, + + // ── Zone 16: cosmic_maelstrom ───────────────────────────────────────────── + { + id: "outer_current", + name: "The Outer Current", + description: "The outermost spiral of the cosmic maelstrom, where the forces are at their most navigable — which still means they routinely shatter planets that wander too close.", + zoneId: "cosmic_maelstrom", + durationSeconds: 108000, // 30h + possibleMaterials: [ + { materialId: "maelstrom_debris", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "oc_e1", text: "The outer current carries debris from civilisations that were not careful enough. Some of it is recoverable and valuable.", effect: { type: "gold_gain", amount: 60000000000 } }, + { id: "oc_e2", text: "The outer current decided to keep something of your scouts'. The forces involved were non-negotiable.", effect: { type: "gold_loss", amount: 24000000000 } }, + { id: "oc_e3", text: "Maelstrom debris of unusual density, compressed from something that was once considerably larger.", effect: { type: "material_gain", materialId: "maelstrom_debris", quantity: 4 } }, + { id: "oc_e4", text: "Outer-current essence is kinetic beyond what distillation usually handles. Your alchemist uses a containment array.", effect: { type: "essence_gain", amount: 1200000000 } }, + ], + }, + { + id: "debris_field", + name: "The Debris Field", + description: "The accumulated wreckage of everything the maelstrom has consumed, compressed into a navigable (mostly) field. Your scouts move through it quickly. Things in debris fields become part of the debris field.", + zoneId: "cosmic_maelstrom", + durationSeconds: 216000, // 60h + possibleMaterials: [ + { materialId: "maelstrom_debris", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "force_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "df_e1", text: "The debris field contains the compressed remains of a treasury. The gold is recognisable, barely, but recoverable.", effect: { type: "gold_gain", amount: 120000000000 } }, + { id: "df_e2", text: "The debris field added your scouts' supplies to its collection. The addition was non-optional.", effect: { type: "gold_loss", amount: 48000000000 } }, + { id: "df_e3", text: "A force crystal, grown in the debris field where forces compressed something into something else.", effect: { type: "material_gain", materialId: "force_crystal", quantity: 1 } }, + { id: "df_e4", text: "Debris-field essence is concentrated destruction in harvestable form. Your alchemist treats it very carefully.", effect: { type: "essence_gain", amount: 2400000000 } }, + ], + }, + { + id: "force_confluence", + name: "The Force Confluence", + description: "Where the fundamental forces of the cosmos intersect inside the maelstrom. Gravity and electromagnetism and things that do not have names yet jostle each other here with consequences that exceed polite description.", + zoneId: "cosmic_maelstrom", + durationSeconds: 324000, // 90h + possibleMaterials: [ + { materialId: "force_crystal", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "cosmic_fragment", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "fcon_e1", text: "The forces briefly aligned in a configuration that is locally considered extremely profitable. Your scouts agreed.", effect: { type: "gold_gain", amount: 250000000000 } }, + { id: "fcon_e2", text: "The forces briefly aligned in a configuration that extracted a contribution from your scouts by several fundamental mechanisms simultaneously.", effect: { type: "gold_loss", amount: 100000000000 } }, + { id: "fcon_e3", text: "A cosmic fragment from the confluence's eye — the only point of calm in all of this.", effect: { type: "material_gain", materialId: "cosmic_fragment", quantity: 1 } }, + { id: "fcon_e4", text: "Confluence-essence is the meeting point of all forces. Your alchemist needs a new laboratory to handle it.", effect: { type: "essence_gain", amount: 5000000000 } }, + ], + }, + { + id: "eye_approach", + name: "The Eye Approach", + description: "The path to the maelstrom's impossible centre — the one point of absolute calm surrounded by forces that make galaxies look fragile. Your scouts have never been so far in. They are doing this anyway.", + zoneId: "cosmic_maelstrom", + durationSeconds: 432000, // 120h + possibleMaterials: [ + { materialId: "force_crystal", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "cosmic_fragment", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "eye_e1", text: "The eye of the maelstrom contains everything the maelstrom has been turning toward for aeons. Some of it is gold.", effect: { type: "gold_gain", amount: 500000000000 } }, + { id: "eye_e2", text: "The approach extracted something from your scouts the way all maelstroms do: comprehensively and without asking.", effect: { type: "gold_loss", amount: 200000000000 } }, + { id: "eye_e3", text: "A cosmic fragment from the very eye — where everything in the maelstrom is heading, always.", effect: { type: "material_gain", materialId: "cosmic_fragment", quantity: 1 } }, + { id: "eye_e4", text: "Eye-approach essence is the calm at the centre of every storm. Your alchemist works with remarkable focus after handling it.", effect: { type: "essence_gain", amount: 10000000000 } }, + ], + }, + + // ── Zone 17: primeval_sanctum ───────────────────────────────────────────── + { + id: "first_steps", + name: "The First Steps", + description: "The entrance to the oldest place. The floor here was walked before walking was invented, which is philosophically impossible and physically evident.", + zoneId: "primeval_sanctum", + durationSeconds: 115200, // 32h + possibleMaterials: [ + { materialId: "ancient_dust", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "fs_e1", text: "The first steps led to something that has been waiting for a visitor. The wait was long. The gift is proportional.", effect: { type: "gold_gain", amount: 200000000000 } }, + { id: "fs_e2", text: "The sanctum extracted a first-visit levy. This is the oldest toll road your guild will ever use.", effect: { type: "gold_loss", amount: 80000000000 } }, + { id: "fs_e3", text: "Ancient dust from the very first footfalls. It does not compress. It remembers what it was stepped on by.", effect: { type: "material_gain", materialId: "ancient_dust", quantity: 4 } }, + { id: "fs_e4", text: "First-steps essence carries the age of the beginning. Your alchemist does not speak for three days afterward.", effect: { type: "essence_gain", amount: 4000000000 } }, + ], + }, + { + id: "ancient_archive", + name: "The Ancient Archive", + description: "A collection of records that predate the concept of records. The information stored here concerns things that no longer exist, but the records persist because the sanctum will not let them stop.", + zoneId: "primeval_sanctum", + durationSeconds: 230400, // 64h + possibleMaterials: [ + { materialId: "ancient_dust", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "memory_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "aa_e1", text: "An archived record describes the location of something placed here before the archive existed. Your scouts locate it.", effect: { type: "gold_gain", amount: 400000000000 } }, + { id: "aa_e2", text: "An archived record includes a debt incurred by something. The archive's system has transferred it to your guild. Payment was expected.", effect: { type: "gold_loss", amount: 160000000000 } }, + { id: "aa_e3", text: "A memory shard from an archived moment so old it predates memory itself.", effect: { type: "material_gain", materialId: "memory_shard", quantity: 1 } }, + { id: "aa_e4", text: "Archive-essence carries every moment ever recorded here. There are many moments. The essence is very dense.", effect: { type: "essence_gain", amount: 8000000000 } }, + ], + }, + { + id: "memory_chamber", + name: "The Memory Chamber", + description: "Where the sanctum stores the memory of the first moment of existence. The memory is perfect, complete, and overwhelming. Your scouts spend the minimum time here and speak little for some time after.", + zoneId: "primeval_sanctum", + durationSeconds: 345600, // 96h + possibleMaterials: [ + { materialId: "memory_shard", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "primeval_relic", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "mc_e1", text: "The memory of the first moment briefly showed your guild what came just before it. There was considerable wealth there.", effect: { type: "gold_gain", amount: 800000000000 } }, + { id: "mc_e2", text: "The memory of the first moment included a memory of a debt. Your guild has been paying it across many lifetimes without knowing.", effect: { type: "gold_loss", amount: 320000000000 } }, + { id: "mc_e3", text: "A primeval relic from the memory chamber — the first thing ever used, in the memory of its use.", effect: { type: "material_gain", materialId: "primeval_relic", quantity: 1 } }, + { id: "mc_e4", text: "Memory-chamber essence contains the first thought ever thought. Your alchemist is very careful what they think while holding it.", effect: { type: "essence_gain", amount: 16000000000 } }, + ], + }, + { + id: "the_oldest_place", + name: "The Oldest Place", + description: "There is nothing older than this. The sanctum's deepest point, where the very first thing that ever was still is, unchanged, because nothing in the universe has had long enough to change it.", + zoneId: "primeval_sanctum", + durationSeconds: 460800, // 128h + possibleMaterials: [ + { materialId: "memory_shard", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "primeval_relic", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "top_e1", text: "The first thing that ever was acknowledges your guild. The acknowledgment takes the form of the oldest expression of approval: considerable wealth.", effect: { type: "gold_gain", amount: 1600000000000 } }, + { id: "top_e2", text: "The first thing that ever was notices something of yours and takes it back, as if it was always meant to be here.", effect: { type: "gold_loss", amount: 640000000000 } }, + { id: "top_e3", text: "A primeval relic from the oldest place — the first artefact of the first thing. Yours now.", effect: { type: "material_gain", materialId: "primeval_relic", quantity: 1 } }, + { id: "top_e4", text: "Oldest-place essence is the essence of the very beginning. Your alchemist processes it and immediately retires.", effect: { type: "essence_gain", amount: 32000000000 } }, + ], + }, + + // ── Zone 18: the_absolute ───────────────────────────────────────────────── + { + id: "edge_of_everything", + name: "The Edge of Everything", + description: "The boundary between existence and non-existence. On one side: everything there is. On the other: everything there isn't. The view from here is indescribable and has been described by your scouts at length.", + zoneId: "the_absolute", + durationSeconds: 129600, // 36h + possibleMaterials: [ + { materialId: "absolute_fragment", minQuantity: 3, maxQuantity: 7, weight: 3 }, + ], + events: [ + { id: "eoe_e1", text: "The edge yields something from the other side — from non-existence, which turns out to have things in it.", effect: { type: "gold_gain", amount: 600000000000 } }, + { id: "eoe_e2", text: "Something from non-existence was interested in your scouts' equipment. It has it now.", effect: { type: "gold_loss", amount: 240000000000 } }, + { id: "eoe_e3", text: "Absolute fragments shed from the edge itself, where everything and nothing meet.", effect: { type: "material_gain", materialId: "absolute_fragment", quantity: 4 } }, + { id: "eoe_e4", text: "Edge-essence is the boundary of all that is. Your alchemist works from both sides simultaneously.", effect: { type: "essence_gain", amount: 12000000000 } }, + ], + }, + { + id: "truth_approach", + name: "The Truth Approach", + description: "The road to the final truth, which your guild has been walking toward since the first step in the Verdant Vale. It looks like every other road your guild has walked. It feels different.", + zoneId: "the_absolute", + durationSeconds: 259200, // 72h + possibleMaterials: [ + { materialId: "absolute_fragment", minQuantity: 4, maxQuantity: 9, weight: 3 }, + { materialId: "boundary_shard", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "tra_e1", text: "The approach yields something that has been waiting for exactly your guild, at exactly this moment.", effect: { type: "gold_gain", amount: 1200000000000 } }, + { id: "tra_e2", text: "The approach extracted a toll that seems proportional to how far your guild has come. It is a very large toll.", effect: { type: "gold_loss", amount: 480000000000 } }, + { id: "tra_e3", text: "A boundary shard from where the approach touches the final truth.", effect: { type: "material_gain", materialId: "boundary_shard", quantity: 1 } }, + { id: "tra_e4", text: "Truth-approach essence is the accumulated potential of approaching the absolute. Your alchemist has been waiting for this.", effect: { type: "essence_gain", amount: 24000000000 } }, + ], + }, + { + id: "final_antechamber", + name: "The Final Antechamber", + description: "One step from the absolute. The door ahead is the last door. Your guild has opened every other door. This one opens when you are ready, which is something only the absolute can determine.", + zoneId: "the_absolute", + durationSeconds: 388800, // 108h + possibleMaterials: [ + { materialId: "boundary_shard", minQuantity: 1, maxQuantity: 3, weight: 3 }, + { materialId: "omega_crystal", minQuantity: 1, maxQuantity: 1, weight: 1 }, + ], + events: [ + { id: "fan_e1", text: "The antechamber contains the deferred offerings of every being that was judged ready before your guild. They are yours now.", effect: { type: "gold_gain", amount: 2500000000000 } }, + { id: "fan_e2", text: "The antechamber extracted preparation costs. What lies ahead requires that you come as you are. Lighter.", effect: { type: "gold_loss", amount: 1000000000000 } }, + { id: "fan_e3", text: "An omega crystal from the antechamber floor — left by the last being to stand here before your guild.", effect: { type: "material_gain", materialId: "omega_crystal", quantity: 1 } }, + { id: "fan_e4", text: "Antechamber-essence is final preparation. Your alchemist works on it with the focus of someone who knows this is the last time.", effect: { type: "essence_gain", amount: 50000000000 } }, + ], + }, + { + id: "the_absolute_heart", + name: "The Absolute Heart", + description: "The final truth, at the end of all things. There is nothing beyond this. Your guild stands here, at the end, and finds that the end is not empty. It has been waiting for you specifically.", + zoneId: "the_absolute", + durationSeconds: 518400, // 144h + possibleMaterials: [ + { materialId: "boundary_shard", minQuantity: 2, maxQuantity: 4, weight: 3 }, + { materialId: "omega_crystal", minQuantity: 1, maxQuantity: 2, weight: 2 }, + ], + events: [ + { id: "tah_e1", text: "The absolute heart recognises your guild as having reached it. The recognition is expressed as the final, and largest, reward possible.", effect: { type: "gold_gain", amount: 5000000000000 } }, + { id: "tah_e2", text: "The absolute heart extracted the final toll. Everything ends, including wealth. Temporarily.", effect: { type: "gold_loss", amount: 2000000000000 } }, + { id: "tah_e3", text: "An omega crystal from the absolute heart — the last omega crystal, which is fitting.", effect: { type: "material_gain", materialId: "omega_crystal", quantity: 1 } }, + { id: "tah_e4", text: "Heart-of-the-absolute essence. Your alchemist processes it in silence. Everyone in the guild hall stops what they are doing and watches.", effect: { type: "essence_gain", amount: 100000000000 } }, + ], + }, +]; diff --git a/apps/api/src/data/initialState.ts b/apps/api/src/data/initialState.ts index 003e6be..d01a51a 100644 --- a/apps/api/src/data/initialState.ts +++ b/apps/api/src/data/initialState.ts @@ -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), }); diff --git a/apps/api/src/data/materials.ts b/apps/api/src/data/materials.ts new file mode 100644 index 0000000..096ae8f --- /dev/null +++ b/apps/api/src/data/materials.ts @@ -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" }, +]; diff --git a/apps/api/src/data/recipes.ts b/apps/api/src/data/recipes.ts new file mode 100644 index 0000000..7529a8d --- /dev/null +++ b/apps/api/src/data/recipes.ts @@ -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 }, + }, +]; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 4ef3c70..7fa9c84 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -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); diff --git a/apps/api/src/routes/apotheosis.ts b/apps/api/src/routes/apotheosis.ts index c845de2..c666fe9 100644 --- a/apps/api/src/routes/apotheosis.ts +++ b/apps/api/src/routes/apotheosis.ts @@ -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(); - - 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, diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index 4ef7872..0c06a67 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -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 }; }; diff --git a/apps/api/src/routes/craft.ts b/apps/api/src/routes/craft.ts new file mode 100644 index 0000000..35411b5 --- /dev/null +++ b/apps/api/src/routes/craft.ts @@ -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(); + +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(); + + 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); +}); diff --git a/apps/api/src/routes/explore.ts b/apps/api/src/routes/explore.ts new file mode 100644 index 0000000..0a12eb0 --- /dev/null +++ b/apps/api/src/routes/explore.ts @@ -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(); + +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(); + + 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(); + + 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); +}); diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index 4d02c14..9360c70 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -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(); @@ -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); diff --git a/apps/api/src/routes/prestige.ts b/apps/api/src/routes/prestige.ts index 5656a60..02c83e7 100644 --- a/apps/api/src/routes/prestige.ts +++ b/apps/api/src/routes/prestige.ts @@ -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(); - - 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, diff --git a/apps/api/src/routes/transcendence.ts b/apps/api/src/routes/transcendence.ts index b14850e..0b00229 100644 --- a/apps/api/src/routes/transcendence.ts +++ b/apps/api/src/routes/transcendence.ts @@ -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(); - - 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, diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index c6356c6..67602ad 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -9,6 +9,12 @@ import type { BuyEchoUpgradeResponse, BuyPrestigeUpgradeRequest, BuyPrestigeUpgradeResponse, + CraftRecipeRequest, + CraftRecipeResponse, + ExploreCollectRequest, + ExploreCollectResponse, + ExploreStartRequest, + ExploreStartResponse, LoadResponse, PrestigeRequest, PrestigeResponse, @@ -117,6 +123,30 @@ export const achieveApotheosis = async (body: ApotheosisRequest): Promise => + request("/explore/start", { + method: "POST", + body: JSON.stringify(body), + }); + +export const collectExploration = async ( + body: ExploreCollectRequest, +): Promise => + request("/explore/collect", { + method: "POST", + body: JSON.stringify(body), + }); + +export const craftRecipe = async ( + body: CraftRecipeRequest, +): Promise => + request("/craft", { + method: "POST", + body: JSON.stringify(body), + }); + export const getPublicProfile = async ( discordId: string, ): Promise => diff --git a/apps/web/src/components/game/AboutPanel.tsx b/apps/web/src/components/game/AboutPanel.tsx index c32bfc7..36d8b1a 100644 --- a/apps/web/src/components/game/AboutPanel.tsx +++ b/apps/web/src/components/game/AboutPanel.tsx @@ -33,7 +33,7 @@ const HOW_TO_PLAY = [ }, { title: "⭐ Prestige", - body: "When you've progressed far enough, you can prestige to earn runestones — a permanent currency that persists across all runs. Prestige resets your current run but grants a production multiplier that stacks with every prestige. Name your prestige character to commemorate the run!", + body: "When you've progressed far enough, you can prestige to earn runestones — a permanent currency that persists across all runs. Prestige resets your current run but grants a production multiplier that stacks with every prestige.", }, { title: "🔮 Runestones & Prestige Upgrades", @@ -51,9 +51,17 @@ const HOW_TO_PLAY = [ title: "📅 Daily Challenges", body: "Complete daily challenges for bonus rewards including gold, essence, crystals, and runestones. Challenges reset each day and vary in difficulty. Completing all daily challenges gives an extra bonus reward.", }, + { + title: "🗺️ Exploration", + body: "Send scouts to explore areas within each zone. Explorations run in real-time and reward gold, essence, and crafting materials when collected. Each area has a set duration — short explorations are faster but longer ones offer rarer finds. A 📖 icon marks areas you've collected from at least once, unlocking a Codex entry.", + }, + { + title: "⚗️ Crafting", + body: "Use materials gathered from exploration to craft permanent bonuses. Each recipe provides a multiplier to gold income, essence income, click power, or combat power — all of which stack and persist across prestige runs. Check the Crafting tab to see your material inventory and available recipes per zone.", + }, { title: "📖 Codex", - body: "Defeating bosses, completing quests, acquiring equipment, hiring adventurers, purchasing upgrades, unlocking prestige upgrades, and discovering new zones all permanently unlock lore entries in the Codex. A badge appears on the Codex tab and a toast notification pops up each time new lore is discovered. Collect all 364 entries to build a complete picture of the world of Elysium.", + body: "Defeating bosses, completing quests, acquiring equipment, hiring adventurers, purchasing upgrades, unlocking prestige upgrades, discovering new zones, collecting from exploration areas, and crafting recipes all permanently unlock lore entries in the Codex. A badge appears on the Codex tab and a toast notification pops up each time new lore is discovered. Collect all 472 entries to build a complete picture of the world of Elysium.", }, { title: "☁️ Cloud Saves", diff --git a/apps/web/src/components/game/ApotheosisPanel.tsx b/apps/web/src/components/game/ApotheosisPanel.tsx index 6bc74da..2b0ab3f 100644 --- a/apps/web/src/components/game/ApotheosisPanel.tsx +++ b/apps/web/src/components/game/ApotheosisPanel.tsx @@ -6,7 +6,6 @@ const TOTAL_ECHO_UPGRADES = TRANSCENDENCE_UPGRADES.length; export const ApotheosisPanel = (): React.JSX.Element => { const { state, apotheosis } = useGame(); - const [characterName, setCharacterName] = useState(""); const [isPending, setIsPending] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); @@ -19,11 +18,10 @@ export const ApotheosisPanel = (): React.JSX.Element => { const apotheosisCount = state.apotheosis?.count ?? 0; const handleApotheosis = async (): Promise => { - if (!characterName.trim()) return; setIsPending(true); setError(null); try { - const data = await apotheosis(characterName.trim()); + const data = await apotheosis(); setResult(data.newApotheosisCount); } catch (err) { setError(err instanceof Error ? err.message : "Apotheosis failed"); @@ -75,18 +73,10 @@ export const ApotheosisPanel = (): React.JSX.Element => { {isEligible && (
-

This action is permanent and irreversible. Choose your name for the next cycle:

- { setCharacterName(e.target.value); }} - placeholder="Character name..." - type="text" - value={characterName} - /> +

This action is permanent and irreversible.

+ )} +
+ + ); + })} + + )} + + + + ); +}; diff --git a/apps/web/src/components/game/ExplorationPanel.tsx b/apps/web/src/components/game/ExplorationPanel.tsx new file mode 100644 index 0000000..fa1cf3c --- /dev/null +++ b/apps/web/src/components/game/ExplorationPanel.tsx @@ -0,0 +1,172 @@ +import type { ExploreCollectResponse } from "@elysium/types"; +import { useState } from "react"; +import { useGame } from "../../context/GameContext.js"; +import { EXPLORATION_AREAS } from "../../data/explorations.js"; +import { ZoneSelector } from "./ZoneSelector.js"; + +const formatDuration = (seconds: number): string => { + if (seconds >= 86400) { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + return hours > 0 ? `${days}d ${hours}h` : `${days}d`; + } + if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; + if (seconds >= 60) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; + return `${seconds}s`; +}; + +const timeRemaining = (startedAt: number, durationSeconds: number): number => { + const elapsed = (Date.now() - startedAt) / 1000; + return Math.max(0, durationSeconds - elapsed); +}; + +interface CollectResult { + areaId: string; + response: ExploreCollectResponse; +} + +export const ExplorationPanel = (): React.JSX.Element => { + const { state, startExploration, collectExploration, formatNumber } = useGame(); + const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); + const [pendingAreaId, setPendingAreaId] = useState(null); + const [lastResult, setLastResult] = useState(null); + + if (!state) return

Loading...

; + + const zones = state.zones ?? []; + const explorationState = state.exploration; + + const zoneAreas = EXPLORATION_AREAS.filter((a) => a.zoneId === activeZoneId); + + const handleStart = async (areaId: string): Promise => { + setPendingAreaId(areaId); + try { + await startExploration(areaId); + } finally { + setPendingAreaId(null); + } + }; + + const handleCollect = async (areaId: string): Promise => { + setPendingAreaId(areaId); + try { + const result = await collectExploration(areaId); + setLastResult({ areaId, response: result }); + } finally { + setPendingAreaId(null); + } + }; + + return ( +
+
+

🗺️ Exploration

+
+ + {lastResult && ( +
+ + {lastResult.response.foundNothing ? ( +

{lastResult.response.nothingMessage}

+ ) : ( + <> + {lastResult.response.event && ( +

{lastResult.response.event.text}

+ )} +
+ {(lastResult.response.event?.goldChange ?? 0) !== 0 && ( + 0 ? "" : "negative"}`}> + 🪙 {(lastResult.response.event?.goldChange ?? 0) > 0 ? "+" : ""}{formatNumber(lastResult.response.event?.goldChange ?? 0)} gold + + )} + {(lastResult.response.event?.essenceChange ?? 0) > 0 && ( + + ✨ +{formatNumber(lastResult.response.event?.essenceChange ?? 0)} essence + + )} + {lastResult.response.event?.materialGained && ( + + 📦 +{lastResult.response.event.materialGained.quantity} {lastResult.response.event.materialGained.materialId.replace(/_/g, " ")} (event) + + )} + {lastResult.response.materialsFound.map((m) => ( + + 📦 +{m.quantity} {m.materialId.replace(/_/g, " ")} + + ))} +
+ + )} +
+ )} + + { setActiveZoneId(id); setLastResult(null); }} + /> + +
+ {zoneAreas.map((area) => { + const areaState = explorationState?.areas.find((a) => a.id === area.id); + const status = areaState?.status ?? "locked"; + const startedAt = areaState?.startedAt ?? 0; + const isReady = status === "in_progress" && timeRemaining(startedAt, area.durationSeconds) <= 0; + const isPending = pendingAreaId === area.id; + + return ( +
+
+

+ {area.name} + {areaState?.completedOnce && 📖} +

+

{area.description}

+ ⏱️ {formatDuration(area.durationSeconds)} +
+
+ {status === "locked" && ( + 🔒 Locked + )} + {status === "available" && ( + + )} + {status === "in_progress" && !isReady && ( + + ⏳ {formatDuration(Math.ceil(timeRemaining(startedAt, area.durationSeconds)))} remaining + + )} + {(status === "in_progress" && isReady) && ( + + )} +
+
+ ); + })} + {zoneAreas.length === 0 && ( +

No exploration areas in this zone.

+ )} +
+
+ ); +}; diff --git a/apps/web/src/components/game/GameLayout.tsx b/apps/web/src/components/game/GameLayout.tsx index 072ece5..c229aaf 100644 --- a/apps/web/src/components/game/GameLayout.tsx +++ b/apps/web/src/components/game/GameLayout.tsx @@ -20,8 +20,10 @@ import { QuestPanel } from "./QuestPanel.js"; import { StatisticsPanel } from "./StatisticsPanel.js"; import { UpgradePanel } from "./UpgradePanel.js"; import { DailyChallengePanel } from "./DailyChallengePanel.js"; +import { ExplorationPanel } from "./ExplorationPanel.js"; +import { CraftingPanel } from "./CraftingPanel.js"; -type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "apotheosis" | "statistics" | "daily" | "codex" | "about"; +type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "apotheosis" | "statistics" | "daily" | "codex" | "about" | "exploration" | "crafting"; const BASE_TABS: { id: Tab; label: string }[] = [ { id: "adventurers", label: "⚔️ Adventurers" }, @@ -33,6 +35,8 @@ const BASE_TABS: { id: Tab; label: string }[] = [ { id: "prestige", label: "⭐ Prestige" }, { id: "transcendence", label: "🌌 Transcendence" }, { id: "apotheosis", label: "✨ Apotheosis" }, + { id: "exploration", label: "🗺️ Exploration" }, + { id: "crafting", label: "⚗️ Crafting" }, { id: "statistics", label: "📊 Statistics" }, { id: "daily", label: "📅 Daily" }, { id: "codex", label: "📖 Codex" }, @@ -120,6 +124,8 @@ export const GameLayout = (): React.JSX.Element => { {activeTab === "prestige" && } {activeTab === "transcendence" && } {activeTab === "apotheosis" && } + {activeTab === "exploration" && } + {activeTab === "crafting" && } {activeTab === "statistics" && } {activeTab === "daily" && } {activeTab === "codex" && } diff --git a/apps/web/src/components/game/PrestigePanel.tsx b/apps/web/src/components/game/PrestigePanel.tsx index eb5fba1..d734ee7 100644 --- a/apps/web/src/components/game/PrestigePanel.tsx +++ b/apps/web/src/components/game/PrestigePanel.tsx @@ -41,7 +41,6 @@ const CATEGORY_ORDER: PrestigeUpgradeCategory[] = [ export const PrestigePanel = (): React.JSX.Element => { const { state, reload, formatNumber, buyPrestigeUpgrade, toggleAutoPrestige } = useGame(); - const [characterName, setCharacterName] = useState(""); const [isPending, setIsPending] = useState(false); const [result, setResult] = useState<{ runestones: number; count: number; milestoneRunestones: number } | null>(null); const [prestigeError, setPrestigeError] = useState(null); @@ -61,11 +60,10 @@ export const PrestigePanel = (): React.JSX.Element => { const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1); const handlePrestige = async (): Promise => { - if (!characterName.trim()) return; setIsPending(true); setPrestigeError(null); try { - const data = await prestige({ characterName: characterName.trim() }); + const data = await prestige({}); setResult({ runestones: data.runestones, count: data.newPrestigeCount, milestoneRunestones: data.milestoneRunestones }); await reload(); } catch (err) { @@ -156,18 +154,10 @@ export const PrestigePanel = (): React.JSX.Element => { {isEligible ? (
-

You are ready to prestige! Choose your new character name:

- { setCharacterName(e.target.value); }} - placeholder="Character name..." - type="text" - value={characterName} - /> +

You are ready to prestige!