neon-sprawl/server/NeonSprawl.Server/Game/Encounters/EncounterProgressApi.cs

137 lines
4.8 KiB
C#

using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.Encounters;
/// <summary>Maps <c>GET /game/players/{{id}}/encounter-progress</c> (NEO-108).</summary>
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<EncounterProgressRowJson>(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<string>();
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,
};
}
/// <summary>Preserves commit-time grant order from <see cref="EncounterCompleteEvent.GrantedItems"/> (reward-table apply order).</summary>
private static List<EncounterRewardGrantJson> MapGrantSummary(IReadOnlyList<EncounterGrantApplied> grants)
{
var summary = new List<EncounterRewardGrantJson>(grants.Count);
foreach (var grant in grants)
{
summary.Add(
new EncounterRewardGrantJson
{
ItemId = grant.ItemId,
Quantity = grant.Quantity,
});
}
return summary;
}
}