diff --git a/client/README.md b/client/README.md index cfb8079..256ae1d 100644 --- a/client/README.md +++ b/client/README.md @@ -15,6 +15,39 @@ Do not grow an all-in-one **`main.gd`**. The main scene root should **compose** The main scene uses **`World/IsometricFollowCamera`** (`scripts/isometric_follow_camera.gd`): client-local **isometric follow** for **`Player`** (`NodePath` **`../Player`**), with damped eye motion (`follow_smoothness`, exponential lerp) and **teleport snap** when the eye is farther than **`snap_distance`** from the desired pose (e.g. after **`snap_to_server`**). Framing exports **`follow_distance`**, **`pitch_elevation_deg`**, and **`presentation_yaw_deg`** match the pre–NEON-25 static camera at spawn. **Orbit is off in the prototype:** **`allow_yaw`** is false, **`CameraState.yaw`** stays **0**, and no rotate input is read—enable **`allow_yaw`** later and drive **`_orbit_yaw_rad`** from input on the rig (see **`docs/decomposition/modules/E1_M2_IsometricCameraController.md`**). **`CameraState`** (`scripts/camera_state.gd`) is refreshed every **`_physics_process`** tick on the rig (**`process_physics_priority`** after **`Player`**) so framing tracks **`move_and_slide`** without display-vs-physics jitter. **`ground_pick`** uses the viewport camera; **`main.gd`** sets **`fallback_camera`** to **`World/IsometricFollowCamera/Camera3D`**. +## Prototype district (NEON-29) + +The default play space is a **45 × 45** unit flat floor (~2 000 sq units, ~5× the pre-NEON-29 footprint). All geometry lives under `World/NavigationRegion3D` in `scenes/main.tscn`; `main.gd` rebakes the nav mesh at startup so the scene-file vertices are intentionally stale. + +### Props at a glance + +| Node | Group | Size (m) | World XZ | Purpose | +|------|-------|----------|----------|---------| +| `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 | +| `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 | +| `ObstacleD` | `occluder` | 3 × 2 | (−8, −16) | Far-south occluder | +| `TerracePlatformA` | `walkable` | 10 × 10, h=0.3 m | (13, −14) | Low raised pad, SE; single-step ascent from floor | +| `TerraceStepB` | `walkable` | 6 × 3, h=0.3 m | (−15, 8.5) | Approach step for Platform B; bridges floor → platform | +| `TerracePlatformB` | `walkable` | 6 × 6, h=0.6 m | (−15, 13) | Two-level terrace, NW; reached via TerraceStepB | +| `TerracePlatformC` | `walkable` | 8 × 8, h=0.3 m | (14, 14) | Low broad pad, NE; single-step ascent from floor | + +### Height variation notes + +- **Single-step terraces (A, C):** 0.3 m rise — within `agent_max_climb = 0.35`, so the nav mesh routes directly from floor onto the pad; one click suffices. +- **Two-level terrace B:** floor → `TerraceStepB` (0.3 m) → `TerracePlatformB` (additional 0.3 m). Each transition is within climb limit, so the agent can route the full ascent in a single click-to-move target anywhere on the platform. +- **Occluders** are plain `StaticBody3D` with no `walkable` tag; the nav bake excludes their top surfaces, so they remain true obstacles for routing. + +### Designer / QA limits + +- Floor bounds: **±22.5 m** in X and Z from world origin. Clicks outside the `NavigationRegion3D` boundary (beyond the floor edge) will be rejected by `NavigationAgent3D` path queries. +- The `MoveRejectFarPad` at (9, 9) remains at its original position and exercises the server's per-step distance limit, not map-edge distance. +- New obstacles are tagged `"occluder"` so the NEON-27 occlusion policy and NEON-30 click-through both apply automatically. + ## Authoritative movement (NEON-4, NEON-8) 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 (NEON-4/19); the client **moves** toward that verified position using **`NavigationAgent3D`** + a **baked mesh** instead of teleporting on the **`GET`** (NEON-8). **Boot** `sync_from_server()` **snaps** once so spawn matches the server. diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index e954510..a3e5c6d 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -11,8 +11,8 @@ [ext_resource type="Resource" path="res://resources/isometric_occlusion_policy.tres" id="9_occ_pol"] [sub_resource type="NavigationMesh" id="NavigationMesh_district"] -vertices = PackedVector3Array(-0.6999998, 0.25, -0.6999998, -0.10000038, 0.25, -0.6999998, -0.10000038, 0.25, -9.55, -9.55, 0.25, 0.05000019, -1, 0.25, 0.05000019, -9.55, 0.25, -9.55, 8.75, 0.25, -7.4500003, 8.75, 0.25, -6.4000006, 9.65, 0.25, -6.4000006, 9.65, 0.25, -9.55, 0.5, 0.25, -0.6999998, 6.6499996, 0.25, -5.2000003, 6.200001, 0.25, -5.5, 8.450001, 0.25, -7.75, 6.5, 0.25, -7.75, 6.200001, 0.25, -7.4500003, 7.1000004, 2.8000002, -6.8500004, 7.1000004, 2.8000002, -6.1000004, 7.8500004, 2.8000002, -6.1000004, 7.8500004, 2.8000002, -6.8500004, 7.25, 0.25, -6.7000003, 7.25, 0.25, -6.25, 7.700001, 0.25, -6.25, 7.700001, 0.25, -6.7000003, 8.75, 0.25, -5.5, 7.550001, 0.25, 3.8000002, 7.550001, 0.25, 5, 9.65, 0.25, 5, 7.25, 0.25, 3.5, 0.9499998, 0.25, 0.5, 0.5, 0.25, 0.8000002, 4.55, 0.25, 3.9499998, 4.8500004, 0.25, 3.5, 8.450001, 0.25, -5.2000003, 0.9499998, 0.25, -0.39999962, -0.6999998, 0.25, 0.8000002, 4.55, 0.25, 6.200001, 4.55, 0.25, 9.65, -9.55, 0.25, 9.65, 5.45, 2.2, 4.4000006, 5.45, 2.2, 5.6000004, 6.6499996, 2.2, 5.6000004, 6.6499996, 2.2, 4.4000006, 5.6000004, 0.25, 4.55, 5.6000004, 0.25, 5.45, 6.5, 0.25, 5.45, 6.5, 0.25, 4.55, 7.550001, 0.25, 6.200001, 7.25, 0.25, 6.5, 9.800001, 0.40000004, 9.800001, 4.8500004, 0.25, 6.5) -polygons = [PackedInt32Array(2, 1, 0), PackedInt32Array(4, 3, 0), PackedInt32Array(0, 3, 5), PackedInt32Array(0, 5, 2), PackedInt32Array(7, 6, 8), PackedInt32Array(8, 6, 9), PackedInt32Array(12, 11, 10), PackedInt32Array(9, 6, 13), PackedInt32Array(13, 14, 9), PackedInt32Array(9, 14, 2), PackedInt32Array(10, 1, 12), PackedInt32Array(12, 1, 15), PackedInt32Array(15, 1, 2), PackedInt32Array(2, 14, 15), PackedInt32Array(19, 18, 16), PackedInt32Array(16, 18, 17), PackedInt32Array(23, 22, 20), PackedInt32Array(20, 22, 21), PackedInt32Array(24, 7, 8), PackedInt32Array(27, 26, 25), PackedInt32Array(25, 28, 27), PackedInt32Array(27, 28, 24), PackedInt32Array(27, 24, 8), PackedInt32Array(32, 31, 29), PackedInt32Array(29, 31, 30), PackedInt32Array(28, 33, 24), PackedInt32Array(29, 34, 32), PackedInt32Array(32, 34, 28), PackedInt32Array(10, 11, 34), PackedInt32Array(34, 11, 33), PackedInt32Array(34, 33, 28), PackedInt32Array(30, 31, 35), PackedInt32Array(35, 31, 36), PackedInt32Array(36, 37, 35), PackedInt32Array(35, 37, 3), PackedInt32Array(3, 37, 38), PackedInt32Array(35, 3, 4), PackedInt32Array(42, 41, 39), PackedInt32Array(39, 41, 40), PackedInt32Array(46, 45, 43), PackedInt32Array(43, 45, 44), PackedInt32Array(47, 26, 27), PackedInt32Array(47, 27, 48), PackedInt32Array(48, 27, 49), PackedInt32Array(48, 49, 37), PackedInt32Array(37, 36, 50), PackedInt32Array(37, 50, 48)] +vertices = PackedVector3Array() +polygons = [] geometry_collision_mask = 1 cell_size = 0.15 cell_height = 0.15 @@ -22,10 +22,10 @@ agent_radius = 0.4 agent_max_climb = 0.35 [sub_resource type="BoxMesh" id="BoxMesh_floor"] -size = Vector3(20, 0.2, 20) +size = Vector3(45, 0.2, 45) [sub_resource type="BoxShape3D" id="BoxShape3D_floor"] -size = Vector3(20, 0.2, 20) +size = Vector3(45, 0.2, 45) [sub_resource type="BoxMesh" id="BoxMesh_terminal"] size = Vector3(0.9, 1, 0.4) @@ -78,6 +78,51 @@ height = 1.0 specular_mode = 2 albedo_color = Color(0.42, 0.52, 0.7, 1) +[sub_resource type="BoxMesh" id="BoxMesh_obstacle_b"] +size = Vector3(3, 3, 3) + +[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle_b"] +size = Vector3(3, 3, 3) + +[sub_resource type="BoxMesh" id="BoxMesh_obstacle_c"] +size = Vector3(2, 4, 2) + +[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle_c"] +size = Vector3(2, 4, 2) + +[sub_resource type="BoxMesh" id="BoxMesh_obstacle_d"] +size = Vector3(3, 2, 3) + +[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle_d"] +size = Vector3(3, 2, 3) + +[sub_resource type="StandardMaterial3D" id="Mat_terrace"] +albedo_color = Color(0.55, 0.52, 0.48, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_terrace_a"] +size = Vector3(10, 0.3, 10) + +[sub_resource type="BoxShape3D" id="BoxShape3D_terrace_a"] +size = Vector3(10, 0.3, 10) + +[sub_resource type="BoxMesh" id="BoxMesh_terrace_b_step"] +size = Vector3(6, 0.3, 3) + +[sub_resource type="BoxShape3D" id="BoxShape3D_terrace_b_step"] +size = Vector3(6, 0.3, 3) + +[sub_resource type="BoxMesh" id="BoxMesh_terrace_b_platform"] +size = Vector3(6, 0.6, 6) + +[sub_resource type="BoxShape3D" id="BoxShape3D_terrace_b_platform"] +size = Vector3(6, 0.6, 6) + +[sub_resource type="BoxMesh" id="BoxMesh_terrace_c"] +size = Vector3(8, 0.3, 8) + +[sub_resource type="BoxShape3D" id="BoxShape3D_terrace_c"] +size = Vector3(8, 0.3, 8) + [node name="Main" type="Node3D" unique_id=1358372723] script = ExtResource("1_main") @@ -154,6 +199,87 @@ mesh = SubResource("BoxMesh_obstacle") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) shape = SubResource("BoxShape3D_obstacle") +[node name="ObstacleB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000001 groups=["occluder"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -10, 0, 5) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/ObstacleB" unique_id=3000002] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +mesh = SubResource("BoxMesh_obstacle_b") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/ObstacleB" unique_id=3000003] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +shape = SubResource("BoxShape3D_obstacle_b") + +[node name="ObstacleC" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000004 groups=["occluder"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, -12) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/ObstacleC" unique_id=3000005] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) +mesh = SubResource("BoxMesh_obstacle_c") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/ObstacleC" unique_id=3000006] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0) +shape = SubResource("BoxShape3D_obstacle_c") + +[node name="ObstacleD" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000007 groups=["occluder"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 0, -16) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/ObstacleD" unique_id=3000008] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("BoxMesh_obstacle_d") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/ObstacleD" unique_id=3000009] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("BoxShape3D_obstacle_d") + +[node name="TerracePlatformA" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000010 groups=["walkable"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13, 0, -14) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerracePlatformA" unique_id=3000011] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) +mesh = SubResource("BoxMesh_terrace_a") +surface_material_override/0 = SubResource("Mat_terrace") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/TerracePlatformA" unique_id=3000012] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) +shape = SubResource("BoxShape3D_terrace_a") + +[node name="TerraceStepB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000013 groups=["walkable"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 8.5) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerraceStepB" unique_id=3000014] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) +mesh = SubResource("BoxMesh_terrace_b_step") +surface_material_override/0 = SubResource("Mat_terrace") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/TerraceStepB" unique_id=3000015] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) +shape = SubResource("BoxShape3D_terrace_b_step") + +[node name="TerracePlatformB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000016 groups=["walkable"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 13) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerracePlatformB" unique_id=3000017] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0) +mesh = SubResource("BoxMesh_terrace_b_platform") +surface_material_override/0 = SubResource("Mat_terrace") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/TerracePlatformB" unique_id=3000018] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0) +shape = SubResource("BoxShape3D_terrace_b_platform") + +[node name="TerracePlatformC" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000019 groups=["walkable"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14, 0, 14) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerracePlatformC" unique_id=3000020] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) +mesh = SubResource("BoxMesh_terrace_c") +surface_material_override/0 = SubResource("Mat_terrace") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/TerracePlatformC" unique_id=3000021] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) +shape = SubResource("BoxShape3D_terrace_c") + [node name="InteractionMarkers" type="Node3D" parent="World" unique_id=1700003] script = ExtResource("6_rad")