# Neon Sprawl — Godot client Open this **`client/`** directory as a project in **Godot 4.6** (4.x compatible). Use the **standard** Godot build (not the .NET build)—client code is **GDScript** (`.gd`). - Main scene: `scenes/main.tscn` (bootstrap `scripts/main.gd`). - Networking will use **WebSocket** or **TCP** to the C# server; authoritative logic stays on the server per [`docs/architecture/tech_stack.md`](../docs/architecture/tech_stack.md). ## Script organization (repo policy) Do not grow an all-in-one **`main.gd`**. The main scene root should **compose** child nodes and scripts; put picking, server sync, and similar features in **dedicated scripts** (typically under `scripts/`) and connect via **signals** or small APIs. Use **Autoloads** only when several scenes truly need the same global. Full guidance for contributors and tooling: [`.cursor/rules/godot-client-script-organization.md`](../.cursor/rules/godot-client-script-organization.md). ## Authoritative movement (NS-16, NS-23) With the game server running ([`server/README.md`](../server/README.md)), each valid floor click sends a **`MoveCommand`** (**`POST`**) and a follow-up **`GET`** for **`PositionState`**. The server still **snaps** authority to the target (NS-16/19); the client **moves** toward that verified position using **`NavigationAgent3D`** + a **baked mesh** instead of teleporting on the **`GET`** (NS-23). **Boot** `sync_from_server()` **snaps** once so spawn matches the server. **Tradeoff (prototype):** `player.gd` often steers **straight in xz** toward the goal when the pick’s **Y** is below the capsule origin (smooth **stepped bumps**). **Automatic routing around tall obstacles on one click is not guaranteed** — use **several clicks** to go around e.g. the gray **`Obstacle`** when needed. **Known issue:** visible **idle / rest jitter** on the avatar — follow-up in [NS-24](https://neon-sprawl.atlassian.net/browse/NS-24) (Tech Debt). - **Scripts:** `scripts/ground_pick.gd` (walkable pick + `target_chosen`), `scripts/position_authority_client.gd` (`PositionAuthorityClient`: POST move, GET verify; second signal arg = boot snap vs nav goal), `scripts/player.gd` (path-follow), thin `scripts/main.gd` (nav bake on first frame, then wiring). - **Scene:** `scenes/main.tscn` — walkable **`StaticBody3D`** geometry lives under **`World/NavigationRegion3D`**. `main.gd` calls **`bake_navigation_mesh(false)`** (main-thread bake) after one **`process_frame`**, then waits two **`physics_frame`**s so **`NavigationServer3D`** has a map before agents query paths. After moving floor or obstacles, re-bake in the editor if needed. - **Inspector:** select **`PositionAuthorityClient`** on the main scene to set **`base_url`** (default `http://127.0.0.1:5253`) and **`dev_player_id`** (default `dev-local-1`, must match server `Game:DevPlayerId`). ### Manual check (NS-16 + NS-23) 1. From repo root: `cd server/NeonSprawl.Server && dotnet run` (note the URL/port, usually `http://localhost:5253`). 2. If the port differs, set **`base_url`** on **`PositionAuthorityClient`** accordingly (e.g. `http://127.0.0.1:5253`). 3. Open the client in Godot and run the main scene (**F5**). The player should **snap** to the server’s default position (e.g. **(-5, 0.9, -5)** per `Game:DefaultPosition` / NS-18 walk demo). 4. **Left-click** the floor: the client **POST**s the target, then **GET**s position; the capsule **walks** toward the authoritative target (may follow nav waypoints or bee-line in xz per the tradeoff above). NS-19 reject clicks still show the reject label and do **not** start a path. 5. Click the **far pad** or **pedestal top** (from spawn) to confirm **`horizontal_step_exceeded`** / **`vertical_step_exceeded`** behavior is unchanged. If the server is **down**, boot **`GET`** fails silently (check Output for warnings); clicks while a request is in flight are ignored until it finishes. ## Interaction + range preview (NS-18) The main scene includes a **prototype terminal** at the map center (same world **X/Z** as the server’s static registry) and **two glowing markers** driven by `scripts/interaction_radius_indicators.gd`. That script compares the **`CharacterBody3D`** **`global_position`** (after server snap) to the anchor in **`scripts/prototype_interaction_constants.gd`** using **horizontal distance on X/Z** and the same **`interaction_radius`** as **`PrototypeInteractableRegistry.cs`** — **preview only**; the server **`POST /game/players/{id}/interact`** is authoritative. - **`InteractionRequestClient`** (child of the main scene root): press **E** to POST interact for the prototype id; **Output** prints `allowed` / `reasonCode` (or HTTP errors). While a request is in flight, further **E** presses are ignored (same pattern as **`PositionAuthorityClient`**). - **Inspector:** match **`base_url`** / **`dev_player_id`** to **`PositionAuthorityClient`** and the server `Game:DevPlayerId`. ### Manual check (NS-18) 1. Run the game server and client as in NS-16. 2. **F5** in Godot: default spawn is out of range of the terminal; markers should stay **dim**. **Click-move** toward the center until markers **brighten** (within **3** m on the floor plane). 3. Press **E** (input action **`interact`** in `project.godot`): Output should show **`allowed=true`** when markers glow, **`allowed=false`** with **`reasonCode=out_of_range`** when dim (if you walk back out). Interaction uses **`_input`**, not `_unhandled_input`, so keys register reliably in the embedded **Game** dock; click the game view if the editor had focus elsewhere. ## Movement prototype (NS-14 → NS-23) **`player.gd`** uses **`NavigationAgent3D.get_next_path_position()`** + **`move_and_slide()`** for horizontal motion when following the mesh; it may steer **directly at the goal** in xz when the descend bypass applies (NS-23). **`snap_to_server()`** remains for **boot** (and would apply for any future hard reconcile). - The avatar is on **physics layer 2** with **mask 1** (floor + obstacle on layer 1); the pick ray uses **mask 1** so clicks pass through the avatar and hit the floor. ### Manual check (remnants) 1. Run the main scene (**F5**) **with server running** for movement behavior above. 2. Without the server, startup sync does not apply authoritative position; behavior is undefined for this repo’s intended demo—**run the server** for the demo path. ### Clicks still ignored? In the **Game** dock, **Input** must be active (not the **2D** / **3D** scene-picking tools) so events go to the game. If Input is on and clicks still do nothing, pick rays must use the **viewport’s current camera** and **mouse position** (the script does this so the embedded Game view matches the ray). ## First run 1. Install [Godot 4.x](https://godotengine.org/download). 2. In the project manager, **Import** and select `client/project.godot`. 3. Press **F5** to run the main scene. ## Godot CLI (Linux smoke test) For agents and CI, use the **official Linux x86_64 editor binary** (matches **`config/features`** `4.6` in `project.godot`): 1. Download [Godot_v4.6-stable_linux.x86_64.zip](https://github.com/godotengine/godot/releases/download/4.6-stable/Godot_v4.6-stable_linux.x86_64.zip) from the [4.6-stable release](https://github.com/godotengine/godot/releases/tag/4.6-stable). 2. Unzip and put the binary on your **`PATH`**, e.g. `~/.local/opt/godot-4.6-stable/` and `ln -s …/Godot_v4.6-stable_linux.x86_64 ~/.local/bin/godot`. 3. Ensure **`~/.local/bin`** is on **`PATH`**, then from **`client/`**: ```bash godot --version godot --headless --path . --quit-after 5 ``` A clean checkout has **`client/.godot/`** gitignored, so scripts intentionally avoid **`class_name`** global types that require a full editor import before headless can resolve them.