Gate the EXPLORE slot behind the Special Delivery quest

- Deck-locks the explore action until the 'Special Delivery' story quest is granted, reading the same shared QuestState ledger as the trade unlock so loaded saves see consistent state.
- Adds `_exploreUnlocked()` and `_syncExploreLock()` helpers; re-syncs on quest claim so granting the quest lifts the gate live.
- Refuses direct presses with a console note when the story gate is still closed.
This commit is contained in:
Brian Fertig 2026-09-22 17:59:55 -06:00
parent fededdf590
commit 7aba525aec
1 changed files with 37 additions and 0 deletions

View File

@ -1185,6 +1185,12 @@ export class SurfaceScene extends Phaser.Scene {
// ledger's capitalLanded, 'Special Delivery' step 1) sits on the // ledger's capitalLanded, 'Special Delivery' step 1) sits on the
// shop's Buy/Sell tab (ShopView reads it from the GameScene's ledger, // shop's Buy/Sell tab (ShopView reads it from the GameScene's ledger,
// not this scene, so a save/load can't bypass it). // not this scene, so a save/load can't bypass it).
// The EXPLORE slot stays in deck-locked standby until the 'Special
// Delivery' story quest has begun (granted — the bar's scene is the
// quest's step 1, so the drawer that reaches it waits for the quest;
// the same shared-ledger seam as isTradeUnlocked, so a loaded save
// reads the same truth).
this._syncExploreLock();
// The MENU button keeps the GameScene's save system. The run's state // The MENU button keeps the GameScene's save system. The run's state
// is in the paused GameScene beneath us — SavePanel captures from it // is in the paused GameScene beneath us — SavePanel captures from it
@ -1362,6 +1368,15 @@ export class SurfaceScene extends Phaser.Scene {
return; return;
} }
if (id === 'explore') { if (id === 'explore') {
// The story gate (data/quests.json → special_delivery): the slot is
// deck-locked until the quest is granted, and this check is the
// second door — a press that sneaks past the standby paint still
// lands here and is refused in the console's voice.
if (!this._exploreUnlocked()) {
this.consoleNote('EXPLORATION OFFLINE — THE STORY COMES FIRST');
this.playSfx('ui_close');
return;
}
this.toggleExplore(); this.toggleExplore();
return; return;
} }
@ -1393,6 +1408,27 @@ export class SurfaceScene extends Phaser.Scene {
this.menuSubBar.open(); this.menuSubBar.open();
} }
/**
* Is the EXPLORE slot's story gate open? The 'Special Delivery' quest
* (data/quests.json → special_delivery) granted — QuestState.isGranted
* on the shared registry ledger (the isTradeUnlocked seam), so a
* loaded save reads the same truth as a live run.
*/
_exploreUnlocked() {
return this.gameScene?.questState?.isGranted?.('special_delivery') === true;
}
/**
* Apply the EXPLORE slot's story gate to the deck — standby until the
* 'Special Delivery' quest has begun, live from its grant. Idempotent
* (setSlotEnabled's contract), safe to re-run on a surface claim (the
* grant chain lands on claimQuest, and the surface's QUESTS slot can
* be the one that pays out 'Leaving the Nest').
*/
_syncExploreLock() {
this.actionBar?.setSlotEnabled?.('explore', this._exploreUnlocked());
}
/** /**
* The EXPLORE button: fold the full-width service drawer up / fold it * The EXPLORE button: fold the full-width service drawer up / fold it
* back down (the shop tab bar's span, the same MenuSubBar drawer in * back down (the shop tab bar's span, the same MenuSubBar drawer in
@ -1518,6 +1554,7 @@ export class SurfaceScene extends Phaser.Scene {
isSide ? gid.sideQuestState?.isClaimed?.(id) ?? false : gid.questState?.isClaimed?.(id) ?? false; isSide ? gid.sideQuestState?.isClaimed?.(id) ?? false : gid.questState?.isClaimed?.(id) ?? false;
const before = claimedOf(g); const before = claimedOf(g);
g.claimQuest(id); // the scene's seam (the side dispatch for sq- ids) g.claimQuest(id); // the scene's seam (the side dispatch for sq- ids)
this._syncExploreLock(); // the claim chain can GRANT 'Special Delivery' — lift the EXPLORE gate
const now = claimedOf(g); const now = claimedOf(g);
if (!before && now) { if (!before && now) {
const t = isSide ? config.get('sidequests.ui.claimToast', {}) : config.get('quests.claimToast', {}); const t = isSide ? config.get('sidequests.ui.claimToast', {}) : config.get('quests.claimToast', {});