/** * The quest ledger — the run's quest STATE (pure; the data is * data/quests.json). * * A quest's REQUIREMENTS are not tracked here — they are computed LIVE * from the run when the scene snapshots the dossier (GameScene. * questSnapshot): research progress, lifetime minerals mined from * asteroids, the home world's tether standing, the discovery ledger. * Only the two things the run owns are state: * * granted — which quest ids the player HOLDS (the starter quest is * granted on the run's first boot; future quests are granted * by whatever event issues them) * claimed — which of the held quests have PAID OUT their reward * * …and that is exactly what gets saved (SaveData capture/prepare), so a * save can never hand out a reward twice or un-grant a quest. */ import { config } from '../config/Config.js'; /** @returns {Array} every quest defined (data/quests.json → quests) */ export function questDefs() { const list = config.get('quests.quests', []); return Array.isArray(list) ? list : []; } /** @returns {object|null} the quest definition with this id (or null) */ export function questDef(id) { return questDefs().find((q) => q && q.id === id) ?? null; } /** @returns {string[]} the ids granted on a FRESH run (quests.json → starter) */ export function starterQuestIds() { return questDefs() .filter((q) => q && q.starter === true) .map((q) => q.id); } /** * A held-quest ledger. The ids come from data/quests.json (validated * against it on load — a save pointing at a removed quest just drops it). */ export class QuestState { constructor() { this.granted = new Set(); this.claimed = new Set(); } /** * Grant a quest (idempotent — re-granting one already held is a no-op). * Unknown ids are ignored. @returns {boolean} true when NEWLY granted */ give(id) { if (typeof id !== 'string' || id === '') return false; if (!questDef(id)) return false; if (this.granted.has(id)) return false; this.granted.add(id); return true; } isGranted(id) { return this.granted.has(id); } isClaimed(id) { return this.claimed.has(id); } /** * Mark a quest's reward as paid out (idempotent). Only quests that are * held + fully satisfied should be claimed — the satisfaction is the * scene's call (GameScene.claimQuest checks the live snapshot). * @returns {boolean} true when NEWLY claimed */ claim(id) { if (!this.granted.has(id)) return false; if (this.claimed.has(id)) return false; this.claimed.add(id); return true; } toJSON() { return { granted: [...this.granted], claimed: [...this.claimed] }; } /** * Restore a ledger from a save record (SaveData prepareLoad). A * malformed record yields an empty ledger rather than a throw — quests * are a progression nicety, not a crash vector. */ static fromJSON(data) { const st = new QuestState(); if (!data || typeof data !== 'object') return st; const grant = (arr) => (Array.isArray(arr) ? arr.filter((x) => typeof x === 'string') : []); for (const id of grant(data.granted)) { if (questDef(id)) st.granted.add(id); } for (const id of grant(data.claimed)) { if (st.granted.has(id)) st.claimed.add(id); // claimed ⊆ granted } return st; } }