From ac75af9c73b9d0e185172d98d561ba76bbcd2fed Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sat, 11 Apr 2026 18:17:32 -0400 Subject: [PATCH] NEON-29: remove horizontal step limit; add HorizontalStepEnabled flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expanded 45×45 district made the 18 m MaxHorizontalStep too restrictive (floor diagonal ~63 m). Add HorizontalStepEnabled to MovementValidationOptions (default false, matching DistrictBoundsEnabled pattern) so the check is skipped unless explicitly opted in. Add test for disabled case (9 pass). Update README and plan with the decision. --- client/README.md | 2 +- docs/plans/NEON-29-implementation-plan.md | 7 +++++-- .../PositionState/MoveCommandValidationTests.cs | 13 ++++++++++++- .../Game/PositionState/MoveCommandValidation.cs | 15 +++++++++------ .../PositionState/MovementValidationOptions.cs | 6 +++++- 5 files changed, 32 insertions(+), 11 deletions(-) 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.