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
+13 -5
View File
@@ -157,12 +157,20 @@ export const BossPanel = (): React.JSX.Element => {
for (let i = 0; i < allZoneBosses.length; i++) {
const boss = allZoneBosses[i];
if (!boss || boss.status !== "locked") continue;
if (i === 0 && zone.unlockBossId) {
const gateBoss = state.bosses.find((b) => b.id === zone.unlockBossId);
if (gateBoss) {
bossUnlockHints.set(boss.id, `⚔️ Defeat: ${gateBoss.name}`);
if (i === 0) {
const parts: string[] = [];
if (zone.unlockBossId) {
const gateBoss = state.bosses.find((b) => b.id === zone.unlockBossId);
if (gateBoss) parts.push(`⚔️ Defeat: ${gateBoss.name}`);
}
} else if (i > 0) {
if (zone.unlockQuestId) {
const gateQuest = state.quests.find((q) => q.id === zone.unlockQuestId);
if (gateQuest) parts.push(`📜 Complete: ${gateQuest.name}`);
}
if (parts.length > 0) {
bossUnlockHints.set(boss.id, parts.join(" & "));
}
} else {
const prevBoss = allZoneBosses[i - 1];
if (prevBoss) {
bossUnlockHints.set(boss.id, `⚔️ Defeat: ${prevBoss.name} first`);