namespace NeonSprawl.Server.Game.Encounters; /// /// Encounter activation and defeat-all progress orchestration (NEO-104). /// Combat cast wiring: (NEO-106). /// NEO-109 telemetry hook site: (encounter_start). /// public static class EncounterProgressOperations { /// /// Resolves the first encounter (id order) whose requiredNpcInstanceIds contains . /// When multiple encounters share an NPC, the earliest id wins. /// public static bool TryResolveEncounterForNpc( string npcInstanceId, IEncounterDefinitionRegistry encounterRegistry, out string encounterId, out EncounterDefRow definition) { encounterId = string.Empty; definition = null!; var npcKey = EncounterProgressIds.NormalizeNpcInstanceId(npcInstanceId); if (npcKey.Length == 0) { return false; } foreach (var row in encounterRegistry.GetDefinitionsInIdOrder()) { if (row.RequiredNpcInstanceIds.Any(id => string.Equals( EncounterProgressIds.NormalizeNpcInstanceId(id), npcKey, StringComparison.Ordinal))) { encounterId = row.Id; definition = row; return true; } } return false; } /// /// Activates encounter progress on first engagement with a required NPC. /// No-op when already started, completed, or NPC is not part of any encounter. /// public static bool TryActivateOnFirstEngagement( string playerId, string npcInstanceId, IEncounterDefinitionRegistry encounterRegistry, IEncounterProgressStore progressStore, IEncounterCompletionStore completionStore) { if (!TryResolveEncounterForNpc(npcInstanceId, encounterRegistry, out var encounterId, out _)) { return false; } if (completionStore.IsCompleted(playerId, encounterId)) { return false; } if (!progressStore.TryActivate(playerId, encounterId)) { return false; } // --- Telemetry hook site (NEO-109): future E9.M1 catalog event `encounter_start` --- // TODO(E9.M1): catalog emit — once per player+encounter on first successful TryActivate (anchor matches emit). // Planned payload: playerId, encounterId, npcInstanceId (engagement target), // requiredNpcInstanceIdsCount (catalog requiredNpcInstanceIds for encounterId). No ingest or ILogger here // (comments-only). EncounterCombatWiring calls this on first damaging hit vs a required NPC (NEO-106). return true; } /// /// Records one required NPC defeat. Requires prior activation; idempotent when the NPC was already marked defeated. /// public static bool TryMarkTargetDefeated( string playerId, string npcInstanceId, IEncounterDefinitionRegistry encounterRegistry, IEncounterProgressStore progressStore, IEncounterCompletionStore completionStore) { if (!TryResolveEncounterForNpc(npcInstanceId, encounterRegistry, out var encounterId, out var definition)) { return false; } if (completionStore.IsCompleted(playerId, encounterId)) { return false; } var npcKey = EncounterProgressIds.NormalizeNpcInstanceId(npcInstanceId); if (!definition.RequiredNpcInstanceIds.Any(id => string.Equals(EncounterProgressIds.NormalizeNpcInstanceId(id), npcKey, StringComparison.Ordinal))) { return false; } if (!progressStore.TryGetProgress(playerId, encounterId, out var progress) || !progress.Started) { return false; } if (progress.DefeatedNpcInstanceIds.Contains(npcKey)) { return true; } return progressStore.TryAddDefeatedTarget(playerId, encounterId, npcInstanceId); } /// /// true when every requiredNpcInstanceIds entry is in the defeated set for a started encounter. /// public static bool IsAllRequiredTargetsDefeated( string playerId, string encounterId, IEncounterDefinitionRegistry encounterRegistry, IEncounterProgressStore progressStore) { if (!encounterRegistry.TryNormalizeKnown(encounterId, out var normalizedEncounterId) || !encounterRegistry.TryGetDefinition(normalizedEncounterId, out var definition)) { return false; } if (!progressStore.TryGetProgress(playerId, normalizedEncounterId, out var progress) || !progress.Started) { return false; } var required = new HashSet(StringComparer.Ordinal); foreach (var id in definition.RequiredNpcInstanceIds) { var normalized = EncounterProgressIds.NormalizeNpcInstanceId(id); if (normalized.Length > 0) { required.Add(normalized); } } if (required.Count == 0) { return false; } return required.IsSubsetOf(progress.DefeatedNpcInstanceIds); } }