185 lines
6.8 KiB
JavaScript
185 lines
6.8 KiB
JavaScript
/**
|
|
* 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
|
|
* priority — the ONE priority quest the tracker HUD features (title +
|
|
* live checklist). The player sets/clears it; the ledger
|
|
* also auto-assigns it (see resolvePriority) so there is
|
|
* always a priority while an active story quest exists.
|
|
* cleared — the player CLEARED the priority; the auto-assignment is
|
|
* re-armed by any grant or claim (the active set changed).
|
|
*
|
|
* …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<object>} 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();
|
|
this.priority = null; // the active quest id, or null
|
|
this.priorityCleared = false; // the player declined a priority (sticks until a grant/claim)
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
this.priorityCleared = false; // a new quest re-arms the auto-priority
|
|
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);
|
|
if (this.priority === id) this.priority = null; // it can't feature itself
|
|
this.priorityCleared = false; // the active set changed → re-arm the auto-priority
|
|
return true;
|
|
}
|
|
|
|
// ------------------------------------------------------------ priority
|
|
|
|
/**
|
|
* Set the priority quest (the tracker HUD's featured quest: title +
|
|
* checklist). Must be HELD and not yet CLAIMED. Idempotent — re-setting
|
|
* the current priority is a no-op.
|
|
* @returns {boolean} true when the priority CHANGED
|
|
*/
|
|
setPriority(id) {
|
|
if (typeof id !== 'string' || id === '') return false;
|
|
if (!this.granted.has(id) || this.claimed.has(id)) return false;
|
|
if (this.priority === id) return false;
|
|
this.priority = id;
|
|
this.priorityCleared = false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clear the priority (the player's "no priority right now"). It STICKS
|
|
* — the auto-assignment stays off until a new quest is granted or one
|
|
* is claimed (either re-arms it, the active set changed).
|
|
* @returns {boolean} true when there WAS a priority to clear
|
|
*/
|
|
clearPriority() {
|
|
if (!this.priority) return false;
|
|
this.priority = null;
|
|
this.priorityCleared = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resolve the effective priority from the ACTIVE quests (held + not
|
|
* claimed) in DISPLAY order — main story first, then side quests
|
|
* (data/quests.json order within a category):
|
|
* - the stored priority when it is still active (the player's choice,
|
|
* or the last auto-assignment);
|
|
* - otherwise the FIRST active quest — unless the player cleared the
|
|
* priority (clearPriority), in which case null until a grant/claim;
|
|
* - otherwise null (nothing active).
|
|
* Auto-assignment is persisted on the ledger (idempotent), so the
|
|
* choice survives scene restarts + saves.
|
|
* @param {string[]} activeIds the active quest ids, display order
|
|
* @returns {string|null}
|
|
*/
|
|
resolvePriority(activeIds) {
|
|
const ids = Array.isArray(activeIds) ? activeIds.filter((x) => typeof x === 'string' && x !== '') : [];
|
|
if (this.priority && ids.includes(this.priority)) return this.priority;
|
|
if (this.priorityCleared) return null;
|
|
const first = ids[0] ?? null;
|
|
if (first) {
|
|
this.priority = first;
|
|
this.priorityCleared = false;
|
|
}
|
|
return first;
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
granted: [...this.granted],
|
|
claimed: [...this.claimed],
|
|
priority: this.priority ?? null,
|
|
priorityCleared: this.priorityCleared === true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
// The priority — a save predating it has no field (null). A saved id
|
|
// that is no longer held (or was claimed) is dropped rather than
|
|
// resurrected; the cleared flag survives as stored.
|
|
st.priority =
|
|
typeof data.priority === 'string' && data.priority !== '' &&
|
|
st.granted.has(data.priority) && !st.claimed.has(data.priority)
|
|
? data.priority
|
|
: null;
|
|
st.priorityCleared = data.priorityCleared === true;
|
|
return st;
|
|
}
|
|
}
|