using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Encounters; /// Maps GET /game/players/{{id}}/encounter-progress (NEO-108). public static class EncounterProgressApi { public const string StateInactive = "inactive"; public const string StateActive = "active"; public const string StateCompleted = "completed"; public static WebApplication MapEncounterProgressApi(this WebApplication app) { app.MapGet( "/game/players/{id}/encounter-progress", (string id, IPositionStateStore positions, IEncounterDefinitionRegistry encounterRegistry, IEncounterProgressStore progressStore, IEncounterCompletionStore completionStore, IEncounterCompleteEventStore completeEventStore) => { var trimmedId = id.Trim(); if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _)) { return Results.NotFound(); } return Results.Json( BuildSnapshot( trimmedId, encounterRegistry, progressStore, completionStore, completeEventStore)); }); return app; } internal static EncounterProgressListResponse BuildSnapshot( string playerId, IEncounterDefinitionRegistry encounterRegistry, IEncounterProgressStore progressStore, IEncounterCompletionStore completionStore, IEncounterCompleteEventStore completeEventStore) { var defs = encounterRegistry.GetDefinitionsInIdOrder(); var rows = new List(defs.Count); foreach (var def in defs) { rows.Add( MapRow( playerId, def.Id, progressStore, completionStore, completeEventStore)); } return new EncounterProgressListResponse { PlayerId = playerId, Encounters = rows, SchemaVersion = EncounterProgressListResponse.CurrentSchemaVersion, }; } private static EncounterProgressRowJson MapRow( string playerId, string encounterId, IEncounterProgressStore progressStore, IEncounterCompletionStore completionStore, IEncounterCompleteEventStore completeEventStore) { var hasProgress = progressStore.TryGetProgress(playerId, encounterId, out var progress); var defeatedTargetIds = hasProgress ? [.. progress.DefeatedNpcInstanceIds.OrderBy(static id => id, StringComparer.Ordinal)] : Array.Empty(); if (completionStore.IsCompleted(playerId, encounterId)) { if (!completionStore.TryGetCompletedAt(playerId, encounterId, out var completedAt)) { throw new InvalidOperationException( $"Encounter '{encounterId}' is completed for player '{playerId}' but completedAt is missing."); } if (!completeEventStore.TryGet(playerId, encounterId, out var completeEvent)) { throw new InvalidOperationException( $"Encounter '{encounterId}' is completed for player '{playerId}' but EncounterCompleteEvent is missing."); } return new EncounterProgressRowJson { EncounterId = encounterId, State = StateCompleted, DefeatedTargetIds = defeatedTargetIds, CompletedAt = completedAt, RewardGrantSummary = MapGrantSummary(completeEvent.GrantedItems), }; } if (hasProgress && progress.Started) { return new EncounterProgressRowJson { EncounterId = encounterId, State = StateActive, DefeatedTargetIds = defeatedTargetIds, }; } return new EncounterProgressRowJson { EncounterId = encounterId, State = StateInactive, DefeatedTargetIds = defeatedTargetIds, }; } /// Preserves commit-time grant order from (reward-table apply order). private static List MapGrantSummary(IReadOnlyList grants) { var summary = new List(grants.Count); foreach (var grant in grants) { summary.Add( new EncounterRewardGrantJson { ItemId = grant.ItemId, Quantity = grant.Quantity, }); } return summary; } }