diff --git a/apps/api/src/data/bosses.ts b/apps/api/src/data/bosses.ts index 21144eb..a5a2989 100644 --- a/apps/api/src/data/bosses.ts +++ b/apps/api/src/data/bosses.ts @@ -16,6 +16,7 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: ["click_2"], equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, + zoneId: "verdant_vale", }, { id: "lich_queen", @@ -32,6 +33,7 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: ["global_2"], equipmentRewards: ["enchanted_blade", "plate_armour", "arcane_orb"], prestigeRequirement: 0, + zoneId: "verdant_vale", }, { id: "elder_dragon", @@ -48,6 +50,7 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: ["click_3"], equipmentRewards: ["vorpal_sword", "dragon_scale"], prestigeRequirement: 1, + zoneId: "shattered_ruins", }, { id: "void_titan", @@ -64,5 +67,6 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: [], equipmentRewards: ["philosophers_stone"], prestigeRequirement: 3, + zoneId: "frozen_peaks", }, ]; diff --git a/apps/api/src/data/initialState.ts b/apps/api/src/data/initialState.ts index bc7a35a..73fa901 100644 --- a/apps/api/src/data/initialState.ts +++ b/apps/api/src/data/initialState.ts @@ -5,6 +5,7 @@ import { DEFAULT_BOSSES } from "./bosses.js"; import { DEFAULT_EQUIPMENT } from "./equipment.js"; import { DEFAULT_QUESTS } from "./quests.js"; import { DEFAULT_UPGRADES } from "./upgrades.js"; +import { DEFAULT_ZONES } from "./zones.js"; export const INITIAL_PRESTIGE: PrestigeData = { count: 0, @@ -33,6 +34,7 @@ export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameS equipment: structuredClone(DEFAULT_EQUIPMENT), achievements: structuredClone(DEFAULT_ACHIEVEMENTS), prestige: INITIAL_PRESTIGE, + zones: structuredClone(DEFAULT_ZONES), baseClickPower: 1, lastTickAt: Date.now(), }); diff --git a/apps/api/src/data/quests.ts b/apps/api/src/data/quests.ts index d3a31c3..2f0706d 100644 --- a/apps/api/src/data/quests.ts +++ b/apps/api/src/data/quests.ts @@ -9,6 +9,7 @@ export const DEFAULT_QUESTS: Quest[] = [ durationSeconds: 60, rewards: [{ type: "gold", amount: 500 }], prerequisiteIds: [], + zoneId: "verdant_vale", }, { id: "goblin_camp", @@ -21,6 +22,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "essence", amount: 5 }, ], prerequisiteIds: ["first_steps"], + zoneId: "verdant_vale", }, { id: "haunted_mine", @@ -33,6 +35,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "global_1" }, ], prerequisiteIds: ["goblin_camp"], + zoneId: "verdant_vale", }, { id: "necromancer_tower", @@ -47,6 +50,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "cleric_1" }, ], prerequisiteIds: ["haunted_mine"], + zoneId: "verdant_vale", }, { id: "ancient_ruins", @@ -59,6 +63,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "click_2" }, ], prerequisiteIds: ["haunted_mine"], + zoneId: "verdant_vale", }, { id: "shadow_mere", @@ -72,6 +77,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "scout_1" }, ], prerequisiteIds: ["ancient_ruins"], + zoneId: "shattered_ruins", }, { id: "dragon_lair", @@ -86,6 +92,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "adventurer", targetId: "dragon_rider" }, ], prerequisiteIds: ["ancient_ruins"], + zoneId: "shattered_ruins", }, { id: "frozen_wastes", @@ -100,6 +107,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "global_3" }, ], prerequisiteIds: ["dragon_lair"], + zoneId: "frozen_peaks", }, { id: "void_rift", @@ -114,5 +122,6 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "knight_1" }, ], prerequisiteIds: ["frozen_wastes"], + zoneId: "frozen_peaks", }, ]; diff --git a/apps/api/src/data/zones.ts b/apps/api/src/data/zones.ts new file mode 100644 index 0000000..343b386 --- /dev/null +++ b/apps/api/src/data/zones.ts @@ -0,0 +1,31 @@ +import type { Zone } from "@elysium/types"; + +export const DEFAULT_ZONES: Zone[] = [ + { + id: "verdant_vale", + name: "The Verdant Vale", + description: + "Rolling green hills and ancient forests stretch to the horizon. This is where your guild takes its first steps — trade roads in need of clearing, goblin camps to rout, and an undead queen stirring in the north.", + emoji: "🌿", + status: "unlocked", + unlockBossId: null, + }, + { + id: "shattered_ruins", + name: "The Shattered Ruins", + description: + "The remnants of a civilisation long lost to war and dragonfire. Crumbling towers and cursed lakes hide treasures — and an elder dragon who claims these lands as his own.", + emoji: "🏛️", + status: "locked", + unlockBossId: "lich_queen", + }, + { + id: "frozen_peaks", + name: "The Frozen Peaks", + description: + "At the edge of the world, where the sun barely rises and the cold is a living thing, a tear in reality has drawn something ancient and terrible. Only the mightiest guilds dare tread here.", + emoji: "❄️", + status: "locked", + unlockBossId: "elder_dragon", + }, +]; diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index 683f4e8..26dfab7 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -152,6 +152,13 @@ bossRouter.post("/challenge", async (context) => { nextBoss.status = "available"; } + // Unlock any zone whose unlock condition is this boss + for (const zone of (state.zones ?? [])) { + if (zone.unlockBossId === body.bossId) { + zone.status = "unlocked"; + } + } + rewards = { gold: boss.goldReward, essence: boss.essenceReward, diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index d23e4e9..5708f2e 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -72,6 +72,8 @@ gameRouter.get("/load", async (context) => { // Backfill new quests and upgrades from defaults (add missing ones) const { DEFAULT_QUESTS } = await import("../data/quests.js"); const { DEFAULT_UPGRADES } = await import("../data/upgrades.js"); + const { DEFAULT_ZONES } = await import("../data/zones.js"); + const { DEFAULT_BOSSES } = await import("../data/bosses.js"); for (const defaultQuest of DEFAULT_QUESTS) { if (!state.quests.some((q) => q.id === defaultQuest.id)) { @@ -80,6 +82,15 @@ gameRouter.get("/load", async (context) => { } } + // Backfill zoneId on quests that predate the field + for (const quest of state.quests) { + if (!quest.zoneId) { + const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id); + quest.zoneId = defaults?.zoneId ?? "verdant_vale"; + needsBackfill = true; + } + } + for (const defaultUpgrade of DEFAULT_UPGRADES) { if (!state.upgrades.some((u) => u.id === defaultUpgrade.id)) { state.upgrades.push(structuredClone(defaultUpgrade)); @@ -87,6 +98,30 @@ gameRouter.get("/load", async (context) => { } } + // Backfill zones on saves that predate the feature + if (!Array.isArray(state.zones) || state.zones.length === 0) { + state.zones = structuredClone(DEFAULT_ZONES); + // Infer unlock state from defeated bosses + for (const zone of state.zones) { + if (zone.unlockBossId != null) { + const unlockBoss = state.bosses.find((b) => b.id === zone.unlockBossId); + if (unlockBoss?.status === "defeated") { + zone.status = "unlocked"; + } + } + } + needsBackfill = true; + } + + // Backfill zoneId on bosses that predate the field + for (const boss of state.bosses) { + if (!boss.zoneId) { + const defaults = DEFAULT_BOSSES.find((d) => d.id === boss.id); + boss.zoneId = defaults?.zoneId ?? "verdant_vale"; + needsBackfill = true; + } + } + const now = Date.now(); const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now); diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx index 7149314..0bf2910 100644 --- a/apps/web/src/components/game/BossPanel.tsx +++ b/apps/web/src/components/game/BossPanel.tsx @@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { formatNumber } from "../../utils/format.js"; +import { ZoneSelector } from "./ZoneSelector.js"; interface BossCardProps { boss: Boss; @@ -17,7 +18,7 @@ const BossCard = ({ isChallenging, }: BossCardProps): React.JSX.Element => { const hpPercent = (boss.currentHp / boss.maxHp) * 100; - const isLocked = boss.prestigeRequirement > prestigeCount; + const isPrestigeLocked = boss.prestigeRequirement > prestigeCount; const canChallenge = (boss.status === "available" || boss.status === "in_progress") && !isChallenging; @@ -26,7 +27,7 @@ const BossCard = ({
{boss.description}
- {isLocked && boss.status === "locked" && ( + {isPrestigeLocked && boss.status === "locked" && (🔒 Requires Prestige {boss.prestigeRequirement}
@@ -87,6 +88,7 @@ const BossCard = ({ export const BossPanel = (): React.JSX.Element => { const { state, challengeBoss } = useGame(); const [challengingBossId, setChallengingBossId] = useStateLoading...
No bosses in this zone yet.
+ )}Loading...
No quests in this zone yet.
+ )}