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) =>
{
if (!positions.TryGetPosition(id, out _))
{
return Results.NotFound();
}
var trimmedId = id.Trim();
return Results.Json(BuildSnapshot(trimmedId, registry, xpStore));
});
app.MapPost(
"/game/players/{id}/skill-progression",
(string id, SkillProgressionGrantRequest? body, IPositionStateStore positions,
ISkillDefinitionRegistry registry, IPlayerSkillProgressionStore xpStore) =>
{
var trimmedId = id.Trim();
static SkillProgressionGrantResponse Deny(
SkillProgressionSnapshotResponse snapshot,
string reason) =>
new SkillProgressionGrantResponse
{
Granted = false,
ReasonCode = reason,
Progression = snapshot,
LevelUps = [],
};
if (body is null || body.SchemaVersion != SkillProgressionGrantRequest.CurrentSchemaVersion)
{
return Results.BadRequest();
}
if (!positions.TryGetPosition(id, out _))
{
return Results.NotFound();
}
var beforeSnapshot = BuildSnapshot(trimmedId, registry, xpStore);
if (body.Amount <= 0)
{
return Results.Json(Deny(beforeSnapshot, ReasonInvalidAmount));
}
var skillLookup = body.SkillId.Trim();
if (skillLookup.Length == 0 || !registry.TryGetDefinition(skillLookup, out var def))
{
return Results.Json(Deny(beforeSnapshot, ReasonUnknownSkill));
}
var source = body.SourceKind.Trim();
var allowed = def.AllowedXpSourceKinds.Any(k =>
string.Equals(k, source, StringComparison.OrdinalIgnoreCase));
if (!allowed)
{
return Results.Json(Deny(beforeSnapshot, ReasonSourceKindNotAllowed));
}
if (!xpStore.TryApplyXpDelta(trimmedId, def.Id, body.Amount, out var prevXp, out var newXp))
{
return Results.NotFound();
}
var prevLevel = SkillLevelCurvePlaceholder.LevelFromTotalXp(prevXp);
var nextLevel = SkillLevelCurvePlaceholder.LevelFromTotalXp(newXp);
var levelUps = new List();
if (nextLevel > prevLevel)
{
levelUps.Add(
new SkillLevelUpJson
{
SkillId = def.Id,
PreviousLevel = prevLevel,
NewLevel = nextLevel,
});
}
var afterSnapshot = BuildSnapshot(trimmedId, registry, xpStore);
return Results.Json(
new SkillProgressionGrantResponse
{
Granted = true,
Progression = afterSnapshot,
LevelUps = levelUps,
});
});
return app;
}
internal static SkillProgressionSnapshotResponse BuildSnapshot(
string playerId,
ISkillDefinitionRegistry registry,
IPlayerSkillProgressionStore store)
{
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 = SkillLevelCurvePlaceholder.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,
};
}
}