using NeonSprawl.Server.Game.Mastery; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Skills; /// /// Maps GET/POST /game/players/{{id}}/skill-progression — read snapshot (NEO-37) and XP grant apply (NEO-38). /// public static class SkillProgressionSnapshotApi { /// public const string ReasonUnknownSkill = "unknown_skill"; public const string ReasonSourceKindNotAllowed = "source_kind_not_allowed"; public const string ReasonInvalidAmount = "invalid_amount"; public static WebApplication MapSkillProgressionSnapshotApi(this WebApplication app) { app.MapGet( "/game/players/{id}/skill-progression", (string id, IPositionStateStore positions, ISkillDefinitionRegistry registry, IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve) => { var trimmedId = id.Trim(); if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _)) { return Results.NotFound(); } return Results.Json(BuildSnapshot(trimmedId, registry, xpStore, levelCurve)); }); app.MapPost( "/game/players/{id}/skill-progression", (string id, SkillProgressionGrantRequest? body, IPositionStateStore positions, ISkillDefinitionRegistry registry, IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine) => { if (body is null || body.SchemaVersion != SkillProgressionGrantRequest.CurrentSchemaVersion) { return Results.BadRequest(); } var trimmedId = id.Trim(); if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _)) { return Results.NotFound(); } var outcome = SkillProgressionGrantOperations.TryApplyGrant( trimmedId, body.SkillId, body.Amount, body.SourceKind, registry, xpStore, levelCurve, perkUnlockEngine); return outcome.Kind switch { SkillProgressionGrantApplyKind.Denied => Results.Json(outcome.Response!), SkillProgressionGrantApplyKind.ProgressionStoreMissing => Results.NotFound(), SkillProgressionGrantApplyKind.Granted => Results.Json(outcome.Response!), _ => throw new InvalidOperationException($"Unexpected grant outcome: {outcome.Kind}"), }; }); return app; } internal static SkillProgressionSnapshotResponse BuildSnapshot( string playerId, ISkillDefinitionRegistry registry, IPlayerSkillProgressionStore store, ISkillLevelCurve levelCurve) { var xpBySkill = store.GetXpTotals(playerId); var defs = registry.GetDefinitionsInIdOrder(); var skills = new List(defs.Count); foreach (var d in defs) { xpBySkill.TryGetValue(d.Id, out var xp); var level = levelCurve.LevelFromTotalXp(xp); skills.Add( new SkillProgressionRowJson { Id = d.Id, Xp = xp, Level = level, }); } skills.Sort(static (a, b) => string.CompareOrdinal(a.Id, b.Id)); return new SkillProgressionSnapshotResponse { PlayerId = playerId, Skills = skills, SchemaVersion = SkillProgressionSnapshotResponse.CurrentSchemaVersion, }; } }