using System; using System.Collections.Generic; namespace Jovian.EncounterSystem { /// /// Tracks which encounters the party has resolved and gates any quest /// encounter whose predecessor hasn't fired yet. Consumers use to exclude /// blocked encounters from rolls, and to mark progress. /// /// /// Gating rule: encounter E is gated iff some other quest encounter P has /// P.nextEncounter == E and P hasn't been resolved yet. Build the predecessor map /// once in IndexQuests; rolling and advancement are both O(1). /// public class QuestProgress { private readonly HashSet resolvedIds = new(); private readonly Dictionary predecessorOf = new(); /// Fires when a root quest encounter (no predecessor) is resolved. public event Action QuestStarted; /// Fires when a chained quest encounter is resolved. Args: (previous, current). public event Action QuestAdvanced; /// Fires when a quest encounter with no nextEncounter is resolved. public event Action QuestCompleted; /// Snapshot of every quest encounter id the party has resolved. Save this in the save file. public IReadOnlyCollection ResolvedIds => resolvedIds; /// Construct and index the quest graph from the given collection. /// The collection whose tables will be walked for quest chains. public QuestProgress(EncountersCollection encountersCollection) { IndexQuests(encountersCollection); } /// Returns true if the encounter is currently blocked because its quest /// predecessor hasn't been resolved yet. public bool IsGated(IEncounter encounter) { var id = encounter?.EncounterDefinition?.internalId; if(string.IsNullOrEmpty(id)) { return false; } if(!predecessorOf.TryGetValue(id, out var predecessor)) { return false; } return !resolvedIds.Contains(predecessor.EncounterDefinition.internalId); } /// Inform the progress service that an encounter was just shown to the party. /// Non-quest encounters and already-resolved ones are no-ops. Fires one or two of the /// // events. public void OnEncounterTriggered(IEncounter encounter) { if(encounter?.Kind is not QuestKind questKind) { return; } var id = encounter.EncounterDefinition?.internalId; if(string.IsNullOrEmpty(id) || !resolvedIds.Add(id)) { return; } var next = questKind.nextEncounter.Resolve(); var hasPredecessor = predecessorOf.TryGetValue(id, out var predecessor); if(!hasPredecessor) { QuestStarted?.Invoke(encounter); } else { QuestAdvanced?.Invoke(predecessor, encounter); } if(next == null) { QuestCompleted?.Invoke(encounter); } } /// Restore resolved quest ids from save data. Call after construction, before the /// first . public void LoadResolvedIds(IEnumerable ids) { resolvedIds.Clear(); if(ids == null) { return; } foreach(var id in ids) { if(!string.IsNullOrEmpty(id)) { resolvedIds.Add(id); } } } private void IndexQuests(EncountersCollection collection) { if(collection?.encounterTables == null) { return; } foreach(var table in collection.encounterTables) { if(table?.encounters == null) { continue; } foreach(var encounter in table.encounters) { if(encounter?.Kind is not QuestKind questKind) { continue; } var next = questKind.nextEncounter.Resolve(); if(next == null) { continue; } var nextId = next.EncounterDefinition?.internalId; if(!string.IsNullOrEmpty(nextId)) { predecessorOf[nextId] = encounter; } } } } } }