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;
}
// --- Telemetry hook site (NEO-109): future E9.M1 catalog event `encounter_start` ---
// TODO(E9.M1): catalog emit — when TryActivate returns true (first activation for player+encounter).
// Planned payload: playerId, encounterId, npcInstanceId (engagement target). No ingest or ILogger here
// (comments-only). EncounterCombatWiring calls this on first damaging hit vs a required NPC (NEO-106).
return progressStore.TryActivate(playerId, encounterId);
}
///
/// 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);
}
}