neon-sprawl/client/README.md

80 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 picks **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.
- **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 servers 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 servers 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 repos 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 **viewports 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.