feat: major content expansion with essence and crystal sinks

- 6 zones (up from 3): Shadow Marshes, Volcanic Depths, Astral Void
- 18 bosses (up from 4): 3 per zone with zone-based sequential unlock
- 24 quests (up from 9): 3-4 per zone, reorganised by zone
- 15 adventurer tiers (up from 10): Shadow Assassin through Divine Champion
- 28 equipment pieces (up from 12): boss drops + purchasable with essence/crystals
- 24 upgrades (up from 13): crystal-cost upgrades + new adventurer upgrades
- 22 achievements (up from 14): new milestones for bosses, quests, gold, armies
- Purchasable equipment system: buy items directly with essence or crystals
- Crystal-cost upgrades: spend crystals on global and click power boosts
- Zone-based boss progression: defeating a zone's last boss unlocks the next zone
This commit is contained in:
2026-03-06 14:36:41 -08:00
committed by Naomi Carrigan
parent 653c36c886
commit 772d733e86
15 changed files with 1202 additions and 81 deletions
+57 -4
View File
@@ -69,7 +69,7 @@ gameRouter.get("/load", async (context) => {
}
}
// Backfill new quests and upgrades from defaults (add missing ones)
// Backfill new quests, upgrades, zones, and bosses 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");
@@ -82,10 +82,14 @@ gameRouter.get("/load", async (context) => {
}
}
// Backfill zoneId on quests that predate the field
// Sync zoneId on quests to match current defaults
for (const quest of state.quests) {
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
if (defaults && quest.zoneId !== defaults.zoneId) {
quest.zoneId = defaults.zoneId;
needsBackfill = true;
}
if (!quest.zoneId) {
const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id);
quest.zoneId = defaults?.zoneId ?? "verdant_vale";
needsBackfill = true;
}
@@ -98,7 +102,23 @@ gameRouter.get("/load", async (context) => {
}
}
// Backfill zones on saves that predate the feature
// Backfill costCrystals on upgrades that predate the field
for (const upgrade of state.upgrades) {
if (upgrade.costCrystals == null) {
upgrade.costCrystals = 0;
needsBackfill = true;
}
}
// Merge new adventurers from defaults
for (const defaultAdventurer of DEFAULT_ADVENTURERS) {
if (!state.adventurers.some((a) => a.id === defaultAdventurer.id)) {
state.adventurers.push(structuredClone(defaultAdventurer));
needsBackfill = true;
}
}
// Backfill zones
if (!Array.isArray(state.zones) || state.zones.length === 0) {
state.zones = structuredClone(DEFAULT_ZONES);
// Infer unlock state from defeated bosses
@@ -111,6 +131,22 @@ gameRouter.get("/load", async (context) => {
}
}
needsBackfill = true;
} else {
// Merge new zones from defaults
for (const defaultZone of DEFAULT_ZONES) {
if (!state.zones.some((z) => z.id === defaultZone.id)) {
const newZone = structuredClone(defaultZone);
// Infer unlock state from defeated bosses
if (newZone.unlockBossId != null) {
const unlockBoss = state.bosses.find((b) => b.id === newZone.unlockBossId);
if (unlockBoss?.status === "defeated") {
newZone.status = "unlocked";
}
}
state.zones.push(newZone);
needsBackfill = true;
}
}
}
// Backfill zoneId on bosses that predate the field
@@ -122,6 +158,23 @@ gameRouter.get("/load", async (context) => {
}
}
// Merge new bosses from defaults (new zones' bosses)
for (const defaultBoss of DEFAULT_BOSSES) {
if (!state.bosses.some((b) => b.id === defaultBoss.id)) {
const newBoss = structuredClone(defaultBoss);
// If the zone for this boss is already unlocked, make the first boss in that zone available
const zone = state.zones.find((z) => z.id === newBoss.zoneId);
if (zone?.status === "unlocked") {
const zoneBossesInState = state.bosses.filter((b) => b.zoneId === newBoss.zoneId);
if (zoneBossesInState.length === 0 && newBoss.status === "locked") {
newBoss.status = "available";
}
}
state.bosses.push(newBoss);
needsBackfill = true;
}
}
const now = Date.now();
const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now);