neon-sprawl/server/NeonSprawl.Server/Game/Mastery/PerkStateApi.cs

125 lines
4.4 KiB
C#

using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.Mastery;
/// <summary>
/// Maps <c>GET</c>/<c>POST /game/players/{{id}}/perk-state</c> — read snapshot and branch selection (NEO-48).
/// </summary>
public static class PerkStateApi
{
public static WebApplication MapPerkStateApi(this WebApplication app)
{
app.MapGet(
"/game/players/{id}/perk-state",
(string id, IPositionStateStore positions, IPlayerPerkStateStore perkStore) =>
{
var trimmedId = id.Trim();
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
{
return Results.NotFound();
}
return Results.Json(BuildSnapshot(trimmedId, perkStore));
});
app.MapPost(
"/game/players/{id}/perk-state",
(string id, PerkBranchSelectRequest? body, IPositionStateStore positions, PerkUnlockEngine perkUnlockEngine) =>
{
if (body is null || body.SchemaVersion != PerkBranchSelectRequest.CurrentSchemaVersion)
{
return Results.BadRequest();
}
var trimmedId = id.Trim();
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
{
return Results.NotFound();
}
var outcome = perkUnlockEngine.TrySelectBranch(
trimmedId,
body.SkillId,
body.TierIndex,
body.BranchId);
var perkState = BuildSnapshot(trimmedId, outcome.Snapshot);
if (!outcome.Success)
{
return Results.Json(
new PerkBranchSelectResponse
{
Selected = false,
ReasonCode = outcome.ReasonCode,
PerkState = perkState,
UnlockedEvents = [],
});
}
return Results.Json(
new PerkBranchSelectResponse
{
Selected = true,
PerkState = perkState,
UnlockedEvents = MapEvents(outcome.Events),
});
});
return app;
}
internal static PerkStateSnapshotResponse BuildSnapshot(string playerId, IPlayerPerkStateStore store) =>
BuildSnapshot(playerId, store.GetSnapshot(playerId));
internal static PerkStateSnapshotResponse BuildSnapshot(string playerId, PerkStateSnapshot snapshot)
{
var branchPicks = new List<PerkBranchPickTrackJson>(snapshot.BranchPicksBySkillId.Count);
foreach (var (skillId, tiers) in snapshot.BranchPicksBySkillId.OrderBy(static kv => kv.Key, StringComparer.Ordinal))
{
var picks = tiers
.OrderBy(static kv => kv.Key)
.Select(static kv => new PerkBranchPickRowJson { TierIndex = kv.Key, BranchId = kv.Value })
.ToList();
branchPicks.Add(new PerkBranchPickTrackJson { SkillId = skillId, Picks = picks });
}
var unlocked = snapshot.UnlockedPerkIds.OrderBy(static id => id, StringComparer.Ordinal).ToList();
return new PerkStateSnapshotResponse
{
PlayerId = playerId,
BranchPicks = branchPicks,
UnlockedPerkIds = unlocked,
};
}
private static IReadOnlyList<PerkUnlockedEventJson> MapEvents(IReadOnlyList<PerkUnlockEvent> events)
{
if (events.Count == 0)
{
return [];
}
var mapped = new List<PerkUnlockedEventJson>(events.Count);
foreach (var e in events)
{
mapped.Add(
new PerkUnlockedEventJson
{
PerkId = e.PerkId,
SkillId = e.SkillId,
TierIndex = e.TierIndex,
BranchId = e.BranchId,
Source = e.Source switch
{
PerkUnlockSource.BranchPick => "branch_pick",
PerkUnlockSource.LevelUp => "level_up",
_ => throw new InvalidOperationException($"Unexpected perk unlock source: {e.Source}"),
},
});
}
return mapped;
}
}