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).ToArray()
: 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;
}
}