using Jovian.EncounterSystem; using Jovian.ZoneSystem; using System; using UnityEngine; namespace Nox.Game { public class EncounterHandler { private readonly ZoneSystem zoneSystem; private readonly EncounterRegistry encounterRegistry; private readonly EncounterView encounterView; private readonly AdventureData adventureData; private readonly QuestProgress questProgress; private readonly EncounterResolver resolver; private Vector3 lastKnownPosition; private string previousZoneId; private IEncounter activeEncounter; private int lastCheckedDay; private float nextEncounterRollTime; private bool encounterRolledToday; public EncounterHandler(ZoneSystem zoneSystem, EncounterRegistry encounterRegistry, EncounterPrefabs encounterPrefabs, AdventureData adventureData, QuestProgress questProgress) { this.zoneSystem = zoneSystem; this.encounterRegistry = encounterRegistry; this.adventureData = adventureData; this.questProgress = questProgress; this.resolver = new EncounterResolver(); RegisterHandlers(); encounterView = new EncounterView(encounterPrefabs); encounterView.OptionSelected += OnOptionSelected; lastCheckedDay = adventureData.currentDay; } private void RegisterHandlers() { resolver.Register(HandleChain); resolver.Register(HandleLog); resolver.Register(HandleAbandon); resolver.Register(HandleCombat); resolver.Register(HandleReward); } private bool HandleChain(ChainToEncounterEvent evt, EncounterContext ctx) { if(encounterRegistry.GetEncounters().TryGetValue(evt.nextEncounterId, out var next)) { RecordEncounterCompleted(activeEncounter); TriggerEncounter(next); return true; } return false; } private bool HandleLog(LogEvent evt, EncounterContext ctx) { Debug.Log($"[Encounter '{ctx.CurrentEncounter?.EncounterDefinition?.id}'] {evt.message}"); return false; } private bool HandleAbandon(AbandonEncounterEvent evt, EncounterContext ctx) { FinishEncounter(abandoned: true); return true; } private bool HandleCombat(StartCombatEvent evt, EncounterContext ctx) { Debug.Log($"[Encounter] unhandled event {evt.GetType().Name}"); return false; } private bool HandleReward(GiveRewardEvent evt, EncounterContext ctx) { Debug.Log($"[Encounter] unhandled event {evt.GetType().Name}"); return false; } public bool AskForRandomEncounter(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter) { return ResolveEncounter(zoneContext, encounterTableId, out encounter); } public bool AskForRandomEncounterOfType(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter, IEncounterKind encounterKind) { return ResolveEncounter(zoneContext, encounterTableId, out encounter, encounterKind); } public bool AskForEncounter(string encounterId, out IEncounter encounter) { return encounterRegistry.GetEncounters().TryGetValue(encounterId, out encounter); } private bool ResolveEncounter(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter, IEncounterKind encounterKind = null) { encounter = null; if(zoneContext.isSafe) { return false; } var randomChance = UnityEngine.Random.Range(0f, 1f); var shouldTrigger = randomChance <= zoneContext.finalEncounterChance; if(!shouldTrigger) { Debug.Log($"Rolled for encounter '{encounterTableId}': {randomChance:F2}/{zoneContext.finalEncounterChance:F2} -> none"); return false; } var filter = new Predicate(e => e != null && !IsAlreadyCompleted(e) && !IsGated(e)); if(encounterKind == null) { encounter = encounterRegistry.GetRandomEncounter(filter, encounterTableId); } else { var table = encounterRegistry.GetEncounterTable(encounterTableId); if(table == null) { return false; } for(var attempt = 0; attempt <= table.encounters.Count; attempt++) { encounter = encounterRegistry.GetRandomEncounter(encounterTableId, encounterKind); if(encounter == null || !IsAlreadyCompleted(encounter)) { break; } } if(encounter != null && IsAlreadyCompleted(encounter)) { return false; } } var name = encounter?.EncounterDefinition?.name ?? "none"; Debug.Log($"Rolled for encounter '{encounterTableId}': {randomChance:F2}/{zoneContext.finalEncounterChance:F2} -> {name}"); return encounter != null; } private bool IsAlreadyCompleted(IEncounter encounter) { var def = encounter?.EncounterDefinition; if(def == null || string.IsNullOrEmpty(def.id)) { return false; } // no properties → defaults to repeatable (true). So can't be "already completed". if(encounter.EncounterProperties is not { isRepeatable: false }) { return false; } // abandoned non-repeatable encounters are re-triggerable if(adventureData.abandonedEncounterIds.Contains(def.id)) { return false; } return adventureData.completedEncounterIds.Contains(def.id); } private bool IsGated(IEncounter encounter) { return questProgress != null && questProgress.IsGated(encounter); } private void RecordEncounterCompleted(IEncounter encounter) { var def = encounter?.EncounterDefinition; if(def == null || string.IsNullOrEmpty(def.id)) { return; } // only record non-repeatable encounters that have explicit properties if(encounter.EncounterProperties is not { isRepeatable: false }) { return; } if(!adventureData.completedEncounterIds.Contains(def.id)) { adventureData.completedEncounterIds.Add(def.id); Debug.Log($"Recorded non-repeatable encounter: {def.id}"); } } private void RecordEncounterAbandoned(IEncounter encounter) { var def = encounter?.EncounterDefinition; if(def == null || string.IsNullOrEmpty(def.id)) { return; } if(!adventureData.abandonedEncounterIds.Contains(def.id)) { adventureData.abandonedEncounterIds.Add(def.id); Debug.Log($"Recorded abandoned encounter: {def.id}"); questProgress?.OnEncounterAbandoned(encounter); } } private void TrackZoneTransition(Vector3 position) { var zoneContext = zoneSystem.QueryZone(position); var currentZoneId = zoneContext.resolvedZoneId; if(currentZoneId != previousZoneId) { if(!string.IsNullOrEmpty(currentZoneId)) { Debug.Log($"Entered zone: {currentZoneId} (encounter: {zoneContext.encounterTableId}, safe: {zoneContext.isSafe})"); if(!zoneContext.isSafe) { RollForEncounter(zoneContext); } } else if(!string.IsNullOrEmpty(previousZoneId)) { Debug.Log($"Left zone: {previousZoneId}"); } previousZoneId = currentZoneId; } } private void RollForEncounter(ZoneContext zoneContext) { if(string.IsNullOrEmpty(zoneContext.resolvedZoneId) || zoneContext.isSafe) { return; } if(ResolveEncounter(zoneContext, zoneContext.encounterTableId, out var encounter)) { TriggerEncounter(encounter); } } private void TriggerEncounter(IEncounter encounter) { switch(encounter.EncounterDefinition.Kind) { case CombatKind: return; default: activeEncounter = encounter; adventureData.isEncounterActive = true; questProgress?.OnEncounterTriggered(encounter); encounterView?.SetCurrentEncounter(encounter); encounterView?.Show(); break; } } private void FinishEncounter(bool abandoned = false) { if(abandoned) { RecordEncounterAbandoned(activeEncounter); } else { RecordEncounterCompleted(activeEncounter); } encounterView?.Hide(); activeEncounter = null; adventureData.isEncounterActive = false; } private void OnOptionSelected(int optionIndex) { if(activeEncounter == null) { return; } var options = activeEncounter.EncounterDialogOptionSet?.options; if(options == null || optionIndex < 0 || optionIndex >= options.Count) { FinishEncounter(); return; } ResolveOption(activeEncounter, options[optionIndex]); } private void ResolveOption(IEncounter encounter, EncounterDialogOption option) { if(option?.events == null) { FinishEncounter(); return; } var context = new EncounterContext(encounter) { Registry = encounterRegistry }; resolver.Resolve(option.events, context); if(!context.ShouldStopEvents) { FinishEncounter(); } } public void CheckForEncounters(Vector3 position) { lastKnownPosition = position; TrackZoneTransition(position); } public void Tick() { if(adventureData.currentDay != lastCheckedDay) { nextEncounterRollTime = UnityEngine.Random.Range(adventureData.currentTime, 1f); lastCheckedDay = adventureData.currentDay; encounterRolledToday = false; } if(!encounterRolledToday && adventureData.currentTime >= nextEncounterRollTime) { encounterRolledToday = true; RollForEncounter(zoneSystem.QueryZone(lastKnownPosition)); } } public void Dispose() { if(encounterView != null) { encounterView.OptionSelected -= OnOptionSelected; encounterView.Dispose(); } activeEncounter = null; adventureData.isEncounterActive = false; } } }