diff --git a/apps/api/src/data/quests.ts b/apps/api/src/data/quests.ts index 888861f..57aa04f 100644 --- a/apps/api/src/data/quests.ts +++ b/apps/api/src/data/quests.ts @@ -1306,6 +1306,54 @@ export const defaultQuests: Array = [ status: "locked", zoneId: "reality_forge", }, + { + combatPowerRequired: 1e59, + description: + "The deep levels of the Forge, where the most experimental realities are stored in a state of near-completion. Your guild discovers that several of these abandoned projects are disturbingly familiar.", + durationSeconds: 24 * 60 * 60, + id: "forge_depths", + name: "The Forge Depths", + prerequisiteIds: [ "forge_chronicle" ], + rewards: [ + { amount: 5e62, type: "gold" }, + { amount: 1.5e59, type: "essence" }, + { amount: 8e54, type: "crystals" }, + ], + status: "locked", + zoneId: "reality_forge", + }, + { + combatPowerRequired: 3e64, + description: + "The underlying structure that defines the laws for every reality the Forge produces — a lattice of constraints so fundamental that violating them would undo everything. Your guild crosses it carefully.", + durationSeconds: 24 * 60 * 60, + id: "prime_matrix", + name: "The Prime Matrix", + prerequisiteIds: [ "forge_depths" ], + rewards: [ + { amount: 2e67, type: "gold" }, + { amount: 6e63, type: "essence" }, + { amount: 3e59, type: "crystals" }, + ], + status: "locked", + zoneId: "reality_forge", + }, + { + combatPowerRequired: 8e69, + description: + "The complete record of every reality the Forge has ever produced, indexed, annotated, and preserved. Your universe has a surprisingly detailed entry with several editorial notes.", + durationSeconds: 24 * 60 * 60, + id: "creation_archive", + name: "The Creation Archive", + prerequisiteIds: [ "prime_matrix" ], + rewards: [ + { amount: 1e72, type: "gold" }, + { amount: 3e68, type: "essence" }, + { amount: 1.5e64, type: "crystals" }, + ], + status: "locked", + zoneId: "reality_forge", + }, // ── Cosmic Maelstrom ────────────────────────────────────────────────────── { combatPowerRequired: 1.8e46, @@ -1406,6 +1454,54 @@ export const defaultQuests: Array = [ status: "locked", zoneId: "cosmic_maelstrom", }, + { + combatPowerRequired: 2e73, + description: + "The deepest layer of the maelstrom — where the storms have been spiralling for so long they have created something resembling order. Your guild navigates it by learning to read the shape of chaos.", + durationSeconds: 24 * 60 * 60, + id: "maelstrom_deep", + name: "The Deep Maelstrom", + prerequisiteIds: [ "maelstrom_codex" ], + rewards: [ + { amount: 5e76, type: "gold" }, + { amount: 1.5e73, type: "essence" }, + { amount: 8e68, type: "crystals" }, + ], + status: "locked", + zoneId: "cosmic_maelstrom", + }, + { + combatPowerRequired: 5e79, + description: + "The point at which all the storm currents converge — not because they are drawn there, but because there is nowhere else left to go. Your guild stands in the geometric centre of cosmic fury.", + durationSeconds: 24 * 60 * 60, + id: "maelstrom_nexus", + name: "The Storm Nexus", + prerequisiteIds: [ "maelstrom_deep" ], + rewards: [ + { amount: 2e83, type: "gold" }, + { amount: 6e79, type: "essence" }, + { amount: 3e75, type: "crystals" }, + ], + status: "locked", + zoneId: "cosmic_maelstrom", + }, + { + combatPowerRequired: 1e86, + description: + "The record of every storm that has ever been — an archive written in lightning, indexed in thunder, preserved in the kind of silence that only exists at the exact centre of infinite noise.", + durationSeconds: 24 * 60 * 60, + id: "storm_chronicle", + name: "The Storm Chronicle", + prerequisiteIds: [ "maelstrom_nexus" ], + rewards: [ + { amount: 1e90, type: "gold" }, + { amount: 3e86, type: "essence" }, + { amount: 1.5e82, type: "crystals" }, + ], + status: "locked", + zoneId: "cosmic_maelstrom", + }, // ── Primeval Sanctum ────────────────────────────────────────────────────── { combatPowerRequired: 7.2e49, @@ -1504,6 +1600,54 @@ export const defaultQuests: Array = [ status: "locked", zoneId: "primeval_sanctum", }, + { + combatPowerRequired: 3e92, + description: + "The deepest chambers of the sanctum — where the primordia are not preserved but still occurring, still being, still becoming for the first and only time. The floor hums with unfinished creation.", + durationSeconds: 24 * 60 * 60, + id: "sanctum_deep", + name: "The Deep Sanctum", + prerequisiteIds: [ "sanctum_chronicle" ], + rewards: [ + { amount: 8e95, type: "gold" }, + { amount: 2.5e92, type: "essence" }, + { amount: 1e88, type: "crystals" }, + ], + status: "locked", + zoneId: "primeval_sanctum", + }, + { + combatPowerRequired: 8e98, + description: + "The crossroads between everything primeval and everything that came after — a threshold so old that every subsequent age of the universe is, from its perspective, still ongoing.", + durationSeconds: 24 * 60 * 60, + id: "sanctum_nexus", + name: "The Primeval Nexus", + prerequisiteIds: [ "sanctum_deep" ], + rewards: [ + { amount: 4e102, type: "gold" }, + { amount: 1.2e99, type: "essence" }, + { amount: 5e94, type: "crystals" }, + ], + status: "locked", + zoneId: "primeval_sanctum", + }, + { + combatPowerRequired: 2e105, + description: + "The sanctum's final gift to those who reached its depths: a full accounting of what it means to have existed before time had opinions about how things should go. Your guild is the first to read it.", + durationSeconds: 24 * 60 * 60, + id: "primeval_archive", + name: "The Primeval Archive", + prerequisiteIds: [ "sanctum_nexus" ], + rewards: [ + { amount: 2e109, type: "gold" }, + { amount: 6e105, type: "essence" }, + { amount: 2.5e101, type: "crystals" }, + ], + status: "locked", + zoneId: "primeval_sanctum", + }, // ── The Absolute ────────────────────────────────────────────────────────── { combatPowerRequired: 3e53, @@ -1601,4 +1745,52 @@ export const defaultQuests: Array = [ status: "locked", zoneId: "the_absolute", }, + { + combatPowerRequired: 5e111, + description: + "Beyond the end of everything, there is more. Not in contradiction — but in the way that answers, once found, reveal the next question. Your guild goes further than the concept of further was designed to accommodate.", + durationSeconds: 24 * 60 * 60, + id: "absolute_beyond", + name: "Beyond the Absolute", + prerequisiteIds: [ "absolute_dominion" ], + rewards: [ + { amount: 1e118, type: "gold" }, + { amount: 3e114, type: "essence" }, + { amount: 1.5e110, type: "crystals" }, + ], + status: "locked", + zoneId: "the_absolute", + }, + { + combatPowerRequired: 1e118, + description: + "The region that exists past the end of existence — a space defined not by what it contains but by being the place where containment no longer applies. Your guild navigates it by not needing it to make sense.", + durationSeconds: 24 * 60 * 60, + id: "absolute_depth", + name: "The Absolute Depth", + prerequisiteIds: [ "absolute_beyond" ], + rewards: [ + { amount: 5e124, type: "gold" }, + { amount: 1.5e121, type: "essence" }, + { amount: 7e116, type: "crystals" }, + ], + status: "locked", + zoneId: "the_absolute", + }, + { + combatPowerRequired: 3e124, + description: + "The final record: not of what happened, but of the fact that it happened at all. A guild from a mortal realm reached the end of all things and chose to keep going. The universe notes this with something that is not quite surprise.", + durationSeconds: 24 * 60 * 60, + id: "absolute_chronicle", + name: "The Absolute Chronicle", + prerequisiteIds: [ "absolute_depth" ], + rewards: [ + { amount: 2e131, type: "gold" }, + { amount: 6e127, type: "essence" }, + { amount: 3e123, type: "crystals" }, + ], + status: "locked", + zoneId: "the_absolute", + }, ]; diff --git a/apps/api/test/routes/debug.spec.ts b/apps/api/test/routes/debug.spec.ts index a0fb33e..dcdcdfe 100644 --- a/apps/api/test/routes/debug.spec.ts +++ b/apps/api/test/routes/debug.spec.ts @@ -881,6 +881,30 @@ describe("debug route", () => { expect(body.bossesPatched).toBe(1); }); + it("patches boss when only equipmentRewards differ (covers savedRewards branch)", async () => { + const state = makeState({ + bosses: [{ id: "troll_king", status: "available", currentHp: 1000, maxHp: 1000, upgradeRewards: ["click_2"], bountyRunestonesClaimed: false, damagePerSecond: 5, goldReward: 10_000, essenceReward: 25, crystalReward: 5, equipmentRewards: [], prestigeRequirement: 0, zoneId: "verdant_vale", bountyRunestones: 1, name: "The Troll King", description: "Gruk the Immovable has terrorised the trade roads for decades. Merchants will pay handsomely for his head." }] as GameState["bosses"], + }); + vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); + vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); + const res = await syncNewContent(); + expect(res.status).toBe(200); + const body = await res.json() as { bossesPatched: number }; + expect(body.bossesPatched).toBe(1); + }); + + it("patches boss when only bountyRunestones differs with all other fields matching", async () => { + const state = makeState({ + bosses: [{ id: "troll_king", status: "available", currentHp: 1000, maxHp: 1000, upgradeRewards: ["click_2"], bountyRunestonesClaimed: false, damagePerSecond: 5, goldReward: 10_000, essenceReward: 25, crystalReward: 5, equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, zoneId: "verdant_vale", bountyRunestones: 99, name: "The Troll King", description: "Gruk the Immovable has terrorised the trade roads for decades. Merchants will pay handsomely for his head." }] as GameState["bosses"], + }); + vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); + vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); + const res = await syncNewContent(); + expect(res.status).toBe(200); + const body = await res.json() as { bossesPatched: number }; + expect(body.bossesPatched).toBe(1); + }); + it("skips boss stat patching for bosses not in defaults", async () => { const state = makeState({ bosses: [{ id: "nonexistent_boss_xyz", status: "available", currentHp: 100, maxHp: 1, upgradeRewards: [], bountyRunestonesClaimed: false, damagePerSecond: 1, goldReward: 1, essenceReward: 1, crystalReward: 1, equipmentRewards: [], prestigeRequirement: 0, zoneId: "old_zone", bountyRunestones: 0, name: "Ghost", description: "Old" }] as GameState["bosses"], diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index 5733027..06ceaa7 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -302,7 +302,7 @@ export const computeEffectiveAdventurerStats = ( const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1; const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; - const prestigeCombatMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count); + const prestigeCombatMultiplier = PRESTIGE_COMBAT_BASE ** state.prestige.count; const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1; const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1; const craftedGoldMultiplier @@ -383,7 +383,7 @@ export const computePartyCombatPower = (state: GameState): number => { } } - const prestigeMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count); + const prestigeMultiplier = PRESTIGE_COMBAT_BASE ** state.prestige.count; const equipmentCombatMultiplier = state.equipment. filter((item) => { @@ -477,7 +477,7 @@ export const computeProjectedRunestones = (state: GameState): number => { : 1; const runestoneMult = gain1Mult * gain2Mult; /* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- optional chained game state field */ - const echoMult: number = state.transcendence?.echoRunestoneMultiplier ?? 1; + const echoMult: number = state.transcendence?.echoPrestigeRunestoneMultiplier ?? 1; return Math.floor(base * runestoneMult * echoMult); };