/** * TutorialGuide — the starter quest's POINTING FINGER: which control is * asking for the player's attention right now, given the run's records. * * Pure: no Phaser, no config, no DOM — node-testable (the QuestState / * ResearchState shape). The scene (GameScene._tutorialTarget) feeds it * the live records and applies the result per frame (ActionBar and * ResearchWindow's setHighlight — the pulsing rims). The targets' names * (the deck slots, the category + node) ride on data/quests.json → * tutorial (the data's master switch + the re-targetable ids). * * The beats (the starter quest's opening arc — research the Tether * Level 2, data/quests.json → start_your_journey): * 'scan' the welcome sequence is acknowledged — oac-intro → * oac-tether → oac-scan all on the run's comms played-ledger * (data/comms.json, trigger 'newGame') — and the player * hasn't pressed SCAN yet; * 'research' OAC's 'research a Tether Level 2 upgrade' line * (oac-upgrade, trigger 'firstScan') is on the ledger and * the research console is CLOSED; * 'console' the same beat with the console OPEN — the window's * setHighlight paints the category tab's rim, and the node's * plate's rim once that category is the one on screen; * null REST — the comms still own the screen (nothing has been * heard yet), or the goal (the named node's research) is in * flight / done. * * Derived from the records only — nothing extra is saved; a loaded run * picks the same beat back up. `scanPressed` is the scene-local one-shot * covering the window between a SCAN press and oac-upgrade's start (the * comm's own played-ledger mark is the authoritative witness once it has * started — from then on the beat is 'research'/'console'). */ /** The 'newGame' welcome sequence, in play order (data/comms.json). */ export const WELCOME_COMMS = ['oac-intro', 'oac-tether', 'oac-scan']; /** The scan reaction (trigger 'firstScan') — the 'research a Tether * Level 2 upgrade' line (data/comms.json). */ export const UPGRADE_COMM = 'oac-upgrade'; /** * @param {{ * enabled?: boolean, false → rest (the data's master switch) * starterGranted?: boolean, the starter quest is on the quest ledger * hasPlayed?: (id: string) => boolean, * the run's comms played-ledger * scanPressed?: boolean, the deck's SCAN has been pressed this beat * consoleOpen?: boolean, the research console is on screen * nodeResolving?: boolean, the goal node's research is in flight/done * }} [r] * @returns {'scan'|'research'|'console'|null} the beat to point at */ export function tutorialTarget(r = {}) { if (r.enabled === false) return null; if (r.starterGranted !== true) return null; if (typeof r.hasPlayed !== 'function') return null; // The goal is underway / done — the finger rests (the completion // ignition + toasts carry the news from here on). if (r.nodeResolving === true) return null; // Beat 2 — OAC has said the 'research a Tether Level 2' line. if (r.hasPlayed(UPGRADE_COMM)) { return r.consoleOpen ? 'console' : 'research'; } // Beat 1 — the welcome is acknowledged and SCAN hasn't been pressed. if (WELCOME_COMMS.every((id) => r.hasPlayed(id)) && r.scanPressed !== true) { return 'scan'; } return null; }