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 Vector3 lastKnownPosition; private string previousZoneId; private IEncounter activeEncounter; private int lastCheckedDay; public EncounterHandler(ZoneSystem zoneSystem, EncounterRegistry encounterRegistry, EncounterPrefabs encounterPrefabs, AdventureData adventureData) { this.zoneSystem = zoneSystem; this.encounterRegistry = encounterRegistry; this.adventureData = adventureData; encounterView = new EncounterView(encounterPrefabs); encounterView.OptionSelected += OnOptionSelected; lastCheckedDay = adventureData.currentDay; } 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)); 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; } return adventureData.completedEncounterIds.Contains(def.id); } 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 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})"); } else if(!string.IsNullOrEmpty(previousZoneId)) { Debug.Log($"Left zone: {previousZoneId}"); } previousZoneId = currentZoneId; } } private void DailyEncounterCheck(Vector3 position) { var zoneContext = zoneSystem.QueryZone(position); 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; encounterView?.SetCurrentEncounter(encounter); encounterView?.Show(); break; } } private void FinishEncounter() { 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; } for(var i = 0; i < option.events.Count; i++) { var encounterEvent = option.events[i]; if(encounterEvent == null) { continue; } switch(encounterEvent) { case ChainToEncounterEvent chain: if(AskForEncounter(chain.nextEncounterId, out var next)) { RecordEncounterCompleted(activeEncounter); TriggerEncounter(next); return; } break; case LogEvent log: Debug.Log($"[Encounter '{encounter.EncounterDefinition.id}'] {log.message}"); break; case StartCombatEvent _: case GiveRewardEvent _: Debug.Log($"[Encounter] unhandled event {encounterEvent.GetType().Name}"); break; } } FinishEncounter(); } public void CheckForEncounters(Vector3 position) { lastKnownPosition = position; TrackZoneTransition(position); } public void Tick() { if(adventureData.currentDay != lastCheckedDay) { lastCheckedDay = adventureData.currentDay; DailyEncounterCheck(lastKnownPosition); } } public void Dispose() { if(encounterView != null) { encounterView.OptionSelected -= OnOptionSelected; encounterView.Dispose(); } activeEncounter = null; adventureData.isEncounterActive = false; } } }