feat: sequential zone unlocking with dual boss+quest gate conditions

Zones now unlock in strict linear order. Each zone requires both the
final boss AND the final quest of the previous zone to be completed:

Verdant Vale → Shattered Ruins (forest_giant + ancient_ruins)
Shattered Ruins → Frozen Peaks (elder_dragon + dragon_lair)
Frozen Peaks → Shadow Marshes (void_titan + storm_citadel)
Shadow Marshes → Volcanic Depths (mud_kraken + plague_ruins)
Volcanic Depths → Astral Void (phoenix_lord + the_forge)

- Zone type gains `unlockQuestId` field alongside `unlockBossId`
- boss.ts checks both conditions before unlocking zone on boss defeat
- tick.ts checks both conditions and unlocks zones + first boss on
  quest completion if the boss condition is already met
- GameContext optimistic update also respects dual-condition logic
- BossPanel zone-gate hints now show both "⚔️ Defeat: X & 📜 Complete: Y"
- game.ts backfill syncs unlockQuestId and re-verifies zone status
  using both conditions, reverting incorrectly-unlocked saves
This commit is contained in:
2026-03-06 16:04:52 -08:00
committed by Naomi Carrigan
parent b9a230f40f
commit e780dc5f6c
7 changed files with 160 additions and 39 deletions
+15 -4
View File
@@ -279,7 +279,14 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const nextZoneBossId = zoneBosses[zoneIdx + 1]?.id;
// Find newly unlocked zones and their first bosses
const newlyUnlockedZones = (prev.zones ?? []).filter((z) => z.unlockBossId === bossId && z.status === "locked");
// A zone unlocks when BOTH the gate boss is defeated AND the gate quest is completed
const newlyUnlockedZones = (prev.zones ?? []).filter((z) => {
if (z.status !== "locked" || z.unlockBossId !== bossId) return false;
const questOk =
z.unlockQuestId == null ||
prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
return questOk;
});
const newZoneFirstBossIds = newlyUnlockedZones.map((z) => {
const firstBoss = prev.bosses.find((b) => b.zoneId === z.id);
return firstBoss?.id;
@@ -297,9 +304,13 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
}
return b;
}),
zones: (prev.zones ?? []).map((z) =>
z.unlockBossId === bossId ? { ...z, status: "unlocked" as const } : z,
),
zones: (prev.zones ?? []).map((z) => {
if (z.status !== "locked" || z.unlockBossId !== bossId) return z;
const questOk =
z.unlockQuestId == null ||
prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
return questOk ? { ...z, status: "unlocked" as const } : z;
}),
resources: result.rewards
? {
...prev.resources,