neon-sprawl/server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutApi.cs

129 lines
4.7 KiB
C#

using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.AbilityInput;
/// <summary>Maps hotbar loadout read/update APIs (NEO-29).</summary>
public static class HotbarLoadoutApi
{
public const string ReasonSlotOutOfBounds = "slot_out_of_bounds";
public const string ReasonUnknownAbility = "unknown_ability";
public const string ReasonDuplicateSlot = "duplicate_slot";
public static WebApplication MapHotbarLoadoutApi(this WebApplication app)
{
app.MapGet(
"/game/players/{id}/hotbar-loadout",
(string id, IPositionStateStore positions, IPlayerHotbarLoadoutStore store) =>
{
if (!positions.TryGetPosition(id, out _))
{
return Results.NotFound();
}
if (!store.TryGetBindings(id, out var bindings))
{
return Results.NotFound();
}
return Results.Json(BuildLoadoutResponse(id, bindings));
});
app.MapPost(
"/game/players/{id}/hotbar-loadout",
(string id, HotbarLoadoutUpdateRequest? body, IPositionStateStore positions, IPlayerHotbarLoadoutStore store) =>
{
if (body is null || body.SchemaVersion != HotbarLoadoutUpdateRequest.CurrentSchemaVersion)
{
return Results.BadRequest();
}
if (!positions.TryGetPosition(id, out _))
{
return Results.NotFound();
}
if (!store.TryGetBindings(id, out var before))
{
return Results.NotFound();
}
var updates = body.Slots ?? [];
var parsedUpdates = new List<HotbarSlotBinding>(updates.Count);
var seenSlots = new HashSet<int>();
foreach (var update in updates)
{
if (update.SlotIndex < 0 || update.SlotIndex >= HotbarLoadoutResponse.SlotCountV1)
{
return Results.Json(
new HotbarLoadoutUpdateResponse
{
Updated = false,
ReasonCode = ReasonSlotOutOfBounds,
Loadout = BuildLoadoutResponse(id, before),
});
}
if (!seenSlots.Add(update.SlotIndex))
{
return Results.Json(
new HotbarLoadoutUpdateResponse
{
Updated = false,
ReasonCode = ReasonDuplicateSlot,
Loadout = BuildLoadoutResponse(id, before),
});
}
if (string.IsNullOrWhiteSpace(update.AbilityId) ||
!PrototypeAbilityRegistry.TryNormalizeKnown(update.AbilityId, out var normalizedAbilityId))
{
return Results.Json(
new HotbarLoadoutUpdateResponse
{
Updated = false,
ReasonCode = ReasonUnknownAbility,
Loadout = BuildLoadoutResponse(id, before),
});
}
parsedUpdates.Add(new HotbarSlotBinding(update.SlotIndex, normalizedAbilityId));
}
if (!store.TryUpsertBindings(id, parsedUpdates, out var after))
{
return Results.NotFound();
}
return Results.Json(
new HotbarLoadoutUpdateResponse
{
Updated = true,
Loadout = BuildLoadoutResponse(id, after),
});
});
return app;
}
private static HotbarLoadoutResponse BuildLoadoutResponse(string playerId, IReadOnlyDictionary<int, string> bindings)
{
var slots = new List<HotbarSlotStateJson>(HotbarLoadoutResponse.SlotCountV1);
for (var i = 0; i < HotbarLoadoutResponse.SlotCountV1; i++)
{
bindings.TryGetValue(i, out var abilityId);
slots.Add(
new HotbarSlotStateJson
{
SlotIndex = i,
AbilityId = abilityId,
});
}
return new HotbarLoadoutResponse
{
PlayerId = playerId.Trim(),
Slots = slots,
};
}
}