diff --git a/client/README.md b/client/README.md index 256ae1d..2b2d434 100644 --- a/client/README.md +++ b/client/README.md @@ -26,7 +26,7 @@ The default play space is a **45 × 45** unit flat floor (~2 000 sq units, ~5× | `Floor` | `walkable` | 45 × 45 | (0, 0) | Main play surface | | `PrototypeTerminal` | `walkable` | 0.9 × 0.4 | (0, 0) | Interaction target | | `MoveRejectPedestal` | `walkable` | 1.5 × 1.5 | (7.5, −6.5) | NEON-7 vertical reject | -| `MoveRejectFarPad` | `walkable` | 2.5 × 2.5 | (9, 9) | NEON-7 range reject | +| `MoveRejectFarPad` | `walkable` | 2.5 × 2.5 | (9, 9) | NEON-7 vertical reject pad (horizontal limit removed NEON-29) | | `Obstacle` | `occluder` | 2 × 2 | (6, 5) | Original occlusion / click-through target | | `ObstacleB` | `occluder` | 3 × 3 | (−10, 5) | Second occluder (wider angle from spawn) | | `ObstacleC` | `occluder` | 2 × 4 | (5, −12) | Tall occluder, south quadrant | diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 0a379da..1415701 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -102,8 +102,11 @@ No new automated GDScript tests are added for this story — there is no new GDS --- +## Decisions + +- **No horizontal step limit (resolved):** The server's `MaxHorizontalStep = 18 m` was too restrictive for the expanded 45 × 45 m floor (diagonal ≈ 63 m). Added `HorizontalStepEnabled` bool to `MovementValidationOptions` (defaults `false`), matching the `DistrictBoundsEnabled` pattern. The horizontal check is now skipped by default; the property remains available to opt back in via config. `MoveRejectFarPad` no longer exercises a horizontal range reject; it retains its walkable surface for vertical-step regression only. + ## Open questions / risks - **NavMesh bake time:** A 45×45 floor with several terraces will bake more polygons than the 20×20 baseline. Expected to be negligible for `bake_navigation_mesh(false)` on desktop, but monitor Output for a perceptible pause. -- **NEON-7 reject range change:** The server's `MaxDistance` is currently tuned for the old district. Clicks from the far corners of the new floor will be within server range (they're not "too far" in server terms — the server checks xz step size from _current_ position to _target_, not total map size). Confirm no new silent-reject edge cases; document if found. -- **Agent climb on terraces:** `agent_max_climb = 0.35` limits single-step height. Ramps must grade gently enough for the bake to include them as traversable, or use stacked step geometry (~0.30 m rise per step). Verify in Godot editor nav mesh debug overlay before shipping. +- **Agent climb on terraces:** `agent_max_climb = 0.35` limits single-step height. Each terrace transition is 0.30 m; verify in Godot editor nav mesh debug overlay before shipping. diff --git a/server/NeonSprawl.Server.Tests/Game/PositionState/MoveCommandValidationTests.cs b/server/NeonSprawl.Server.Tests/Game/PositionState/MoveCommandValidationTests.cs index 7614c42..4d45c09 100644 --- a/server/NeonSprawl.Server.Tests/Game/PositionState/MoveCommandValidationTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/PositionState/MoveCommandValidationTests.cs @@ -5,9 +5,11 @@ namespace NeonSprawl.Server.Tests.Game.PositionState; public class MoveCommandValidationTests { - private static MovementValidationOptions Rules(double maxH, double maxV, bool bounds = false) => + private static MovementValidationOptions Rules( + double maxH, double maxV, bool bounds = false, bool horizontalEnabled = true) => new() { + HorizontalStepEnabled = horizontalEnabled, MaxHorizontalStep = maxH, MaxVerticalStep = maxV, DistrictBoundsEnabled = bounds, @@ -63,6 +65,15 @@ public class MoveCommandValidationTests Assert.Equal(MoveCommandReasonCodes.HorizontalStepExceeded, code); } + [Fact] + public void TryValidate_ShouldAllow_WhenHorizontalDisabled() + { + var from = new PositionSnapshot(0, 0, 0, 0); + var to = new PositionVector { X = 1000, Y = 0, Z = 1000 }; + Assert.True(MoveCommandValidation.TryValidate(from, to, Rules(1, 10, horizontalEnabled: false), out var code)); + Assert.Equal("", code); + } + [Fact] public void TryValidate_ShouldIgnoreBounds_WhenDistrictDisabled() { diff --git a/server/NeonSprawl.Server/Game/PositionState/MoveCommandValidation.cs b/server/NeonSprawl.Server/Game/PositionState/MoveCommandValidation.cs index ef75be9..1cadafa 100644 --- a/server/NeonSprawl.Server/Game/PositionState/MoveCommandValidation.cs +++ b/server/NeonSprawl.Server/Game/PositionState/MoveCommandValidation.cs @@ -18,13 +18,16 @@ public static class MoveCommandValidation { reasonCode = ""; - var dx = to.X - from.X; - var dz = to.Z - from.Z; - var horizontal = Math.Sqrt(dx * dx + dz * dz); - if (horizontal > rules.MaxHorizontalStep) + if (rules.HorizontalStepEnabled) { - reasonCode = MoveCommandReasonCodes.HorizontalStepExceeded; - return false; + var dx = to.X - from.X; + var dz = to.Z - from.Z; + var horizontal = Math.Sqrt(dx * dx + dz * dz); + if (horizontal > rules.MaxHorizontalStep) + { + reasonCode = MoveCommandReasonCodes.HorizontalStepExceeded; + return false; + } } var dy = Math.Abs(to.Y - from.Y); diff --git a/server/NeonSprawl.Server/Game/PositionState/MovementValidationOptions.cs b/server/NeonSprawl.Server/Game/PositionState/MovementValidationOptions.cs index 51111e1..2d80cc3 100644 --- a/server/NeonSprawl.Server/Game/PositionState/MovementValidationOptions.cs +++ b/server/NeonSprawl.Server/Game/PositionState/MovementValidationOptions.cs @@ -3,7 +3,11 @@ namespace NeonSprawl.Server.Game.PositionState; /// Per-command movement sanity limits (NS-19). Bound from Game:MovementValidation. public sealed class MovementValidationOptions { - /// Max horizontal displacement on X/Z in one move (meters). Inclusive at the limit. + /// When true, enforces . False (default) means no horizontal limit. + public bool HorizontalStepEnabled { get; set; } + + /// Max horizontal displacement on X/Z in one move (meters). Inclusive at the limit. + /// Only applied when is true. public double MaxHorizontalStep { get; set; } = 18.0; /// Max absolute delta on Y in one move (meters). Inclusive at the limit.