feat: add zone system to bosses and quests

This commit is contained in:
2026-03-06 13:42:40 -08:00
committed by Naomi Carrigan
parent e9e0df31fd
commit 897eba5f64
15 changed files with 239 additions and 4 deletions
+4
View File
@@ -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",
},
];
+2
View File
@@ -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(),
});
+9
View File
@@ -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",
},
];
+31
View File
@@ -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",
},
];
+7
View File
@@ -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,
+35
View File
@@ -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);