using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.World; namespace NeonSprawl.Server.Game.Targeting; /// Maps GET/POST …/target selection APIs (NEO-23). public static class TargetingApi { public const string ReasonUnknownTarget = "unknown_target"; public const string ReasonOutOfRange = "out_of_range"; public static WebApplication MapTargetingApi(this WebApplication app) { app.MapGet( "/game/players/{id}/target", (string id, IPositionStateStore positions, IPlayerTargetLockStore locks) => { if (!positions.TryGetPosition(id, out var snap)) { return Results.NotFound(); } var (lockId, seq) = locks.GetLockState(id); var body = PlayerTargetStateReader.Build(id, lockId, seq, in snap); return Results.Json(body); }); app.MapPost( "/game/players/{id}/target/select", (string id, TargetSelectRequest? body, IPositionStateStore positions, IPlayerTargetLockStore locks) => { if (body is null || body.SchemaVersion != TargetSelectRequest.CurrentSchemaVersion) { return Results.BadRequest(); } if (!positions.TryGetPosition(id, out var snap)) { return Results.NotFound(); } // Clear intent: property omitted or JSON null. if (body.TargetId is null) { var cleared = locks.ApplyClear(id); var stateAfter = PlayerTargetStateReader.Build(id, cleared.LockIdLowercase, cleared.Sequence, in snap); return Results.Json( new TargetSelectResponse { SchemaVersion = TargetSelectResponse.CurrentSchemaVersion, SelectionApplied = true, TargetState = stateAfter, }); } var raw = body.TargetId; var trimmed = raw.Trim(); if (trimmed.Length == 0) { return Results.BadRequest(); } var lookupKey = trimmed.ToLowerInvariant(); if (!PrototypeTargetRegistry.TryGet(lookupKey, out var entry)) { var (lockUnknown, seqUnknown) = locks.GetLockState(id); var stateUnknown = PlayerTargetStateReader.Build(id, lockUnknown, seqUnknown, in snap); return Results.Json( new TargetSelectResponse { SchemaVersion = TargetSelectResponse.CurrentSchemaVersion, SelectionApplied = false, TargetState = stateUnknown, ReasonCode = ReasonUnknownTarget, }); } if (!HorizontalReach.IsWithinHorizontalRadius(snap.X, snap.Z, entry.X, entry.Z, entry.LockRadius)) { var (lockFar, seqFar) = locks.GetLockState(id); var stateFar = PlayerTargetStateReader.Build(id, lockFar, seqFar, in snap); return Results.Json( new TargetSelectResponse { SchemaVersion = TargetSelectResponse.CurrentSchemaVersion, SelectionApplied = false, TargetState = stateFar, ReasonCode = ReasonOutOfRange, }); } var applied = locks.ApplySet(id, lookupKey); var stateOk = PlayerTargetStateReader.Build(id, applied.LockIdLowercase, applied.Sequence, in snap); return Results.Json( new TargetSelectResponse { SchemaVersion = TargetSelectResponse.CurrentSchemaVersion, SelectionApplied = true, TargetState = stateOk, }); }); return app; } }