neon-sprawl/server/NeonSprawl.Server/Game/Interaction/InteractionApi.cs

101 lines
4.0 KiB
C#

using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Game.World;
namespace NeonSprawl.Server.Game.Interaction;
/// <summary>Maps <c>POST /game/players/{{id}}/interact</c> (NS-18).</summary>
public static class InteractionApi
{
public const string ReasonOutOfRange = "out_of_range";
public const string ReasonUnknownInteractable = "unknown_interactable";
public static WebApplication MapInteractionApi(this WebApplication app)
{
app.MapPost(
"/game/players/{id}/interact",
(string id, InteractionRequest? body, IPositionStateStore store,
ISkillDefinitionRegistry registry, IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine) =>
{
if (body is null || body.SchemaVersion != InteractionRequest.CurrentSchemaVersion)
{
return Results.BadRequest();
}
var rawId = body.InteractableId;
if (rawId is null)
{
return Results.BadRequest();
}
var interactableKey = rawId.Trim();
if (interactableKey.Length == 0)
{
return Results.BadRequest();
}
var lookupKey = interactableKey.ToLowerInvariant();
var playerKey = id.Trim();
if (!store.TryGetPosition(playerKey, out var snap))
{
return Results.NotFound();
}
if (!PrototypeInteractableRegistry.TryGet(lookupKey, out var entry))
{
return Results.Json(
new InteractionResponse
{
SchemaVersion = InteractionResponse.CurrentSchemaVersion,
Allowed = false,
ReasonCode = ReasonUnknownInteractable,
});
}
if (!HorizontalReach.IsWithinHorizontalRadius(
snap.X,
snap.Z,
entry.X,
entry.Z,
entry.InteractionRadius))
{
return Results.Json(
new InteractionResponse
{
SchemaVersion = InteractionResponse.CurrentSchemaVersion,
Allowed = false,
ReasonCode = ReasonOutOfRange,
});
}
if (string.Equals(entry.Kind, PrototypeInteractableRegistry.ResourceNodeKind, StringComparison.Ordinal))
{
// NEO-41: prototype gather completion — same grant rules/persistence as POST …/skill-progression.
// Interact response stays Allowed=true; grant outcome is intentionally ignored (silent no-op on deny / store miss per plan).
_ = SkillProgressionGrantOperations.TryApplyGrant(
playerKey,
GatherSkillXpConstants.SalvageSkillId,
GatherSkillXpConstants.ActivityXpPerResourceNodeGather,
GatherSkillXpConstants.ActivitySourceKind,
registry,
xpStore,
levelCurve,
perkUnlockEngine);
}
return Results.Json(
new InteractionResponse
{
SchemaVersion = InteractionResponse.CurrentSchemaVersion,
Allowed = true,
InteractableId = lookupKey,
Payload = new InteractionPlaceholderPayload(),
});
});
return app;
}
}