NS-24: Eliminate idle jitter and movement QA bumps (Godot client)
Physics: Jolt 3D; physics_interpolation off; 120 TPS. CharacterBody3D idle path uses dual floor_max_angle, rim/straddle and ridged-contact handling, rim settle and random-floor-bump lip/wall escape, floor_block_on_wall, NavigationAgent3D avoidance off, per-node interp off. No global_transform in _process (avoid render/physics ghosting). QA geometry: random_floor_bumps.gd (load from main.gd); each bump on its own StaticBody3D under NavigationRegion3D; random_floor_bump_collision_constants.gd for collider fudge and RANDOM_FLOOR_BUMP_MESH_GROUP. Scene: player safe_margin and mesh Y lift; MoveRejectPedestal / MoveRejectFarPad renamed from NS19*; Mat_player_capsule without polygon_offset (per follow-up). Docs: NS-24 plan + resolution, client/README manual steps, code review 2026-04-05-NS-24.md; server/README bump height note. Code/scene identifiers avoid ticket-based ns19_* naming.pull/25/head
parent
3283f6d04c
commit
7b11d4e238
|
|
@ -15,10 +15,12 @@ With the game server running ([`server/README.md`](../server/README.md)), each v
|
|||
|
||||
**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).
|
||||
**NS-19 / movement QA bumps:** On **run**, **`spawn_short_random_bumps`** adds **two** green cylinders, each on its own **`StaticBody3D`** **sibling** of **`Floor`** under **`NavigationRegion3D`** (**`walkable`** on bump roots — avoids compound **internal-edge** jitter vs floor+cylinder on one body). Bump meshes use Godot group **`random_floor_bump_mesh`**. **Collision radius** = mesh **+ `COLLISION_RADIUS_EXTRA`** (see **`scripts/random_floor_bump_collision_constants.gd`**, capped by **`COLLISION_RADIUS_MAX`**). **`bake_navigation_mesh(false)`** after spawn.
|
||||
|
||||
**Idle stability (NS-24):** **Jolt Physics**; **TPS** **120**. **`physics/common/physics_interpolation`** is **off** — with **on**, small physics **position** changes on bump **edges** were **blended** across render frames and looked like **ghosting / extra jitter**. **`snap_to_server()`** still calls **`reset_physics_interpolation()`** for compatibility if you turn interpolation on later. **`floor_max_angle`** **~50°** walking / **~35°** idle; **loose** angle **~0.8 s** after walk stops. **Walk step assist**. **Idle rim / straddle:** **moving** `floor_max_angle` when floor normal is **shallow** or slide hits mix **floor + wall**. One idle **`move_and_slide()`**, rim **settle**, **`random_floor_bump_mesh`** **lip / rim / vertical-wall** escape (**`IDLE_BUMP_ESCAPE_STEP`**, **`PLAYER_CAPSULE_RADIUS`**, collider fudge from **`random_floor_bump_collision_constants.gd`**). **`Player`:** interp **Off**, **`avoidance_enabled`** **false**, **`floor_block_on_wall`** **true** — do **not** rewrite **`global_transform`** in **`_process`** (can **ghost** **`CharacterBody3D`**). **`_snap_capsule_upright()`** after motion. Idle **`FLOOR_SNAP_IDLE`** ~**11 cm**; walking **`FLOOR_SNAP_MOVING`** ~**0.32**. **Rendering:** **`Mat_player_capsule`**, **`cast_shadow = 0`**, capsule mesh **+Y ~3.4 cm** (visual-only vs **`CapsuleShape3D`** — less **z-fight** vs floor/bump tops), **`light_specular = 0`**, **`msaa_3d = 2`**. **`safe_margin`** **0.055**.
|
||||
|
||||
- **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.
|
||||
- **Scene:** `scenes/main.tscn` — walkable **`StaticBody3D`** geometry lives under **`World/NavigationRegion3D`**. `main.gd` waits one **`process_frame`**, spawns **random test bumps** on **`Floor`**, then **`bake_navigation_mesh(false)`** (main-thread bake), then waits two **`physics_frame`**s so **`NavigationServer3D`** has a map before agents query paths. The baked **`NavigationMesh`** asset in the scene file is **stale** until you run or re-bake in the editor.
|
||||
- **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)
|
||||
|
|
@ -28,6 +30,7 @@ With the game server running ([`server/README.md`](../server/README.md)), each v
|
|||
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.
|
||||
6. After a move (or at spawn), **stand idle** with no click goal for **10+ seconds** and confirm the capsule does **not** visibly vibrate on the floor or the **random green bumps** (positions change each run).
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ interact={
|
|||
|
||||
project/assembly_name="Neon Sprawl"
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
common/physics_ticks_per_second=120
|
||||
common/physics_interpolation=false
|
||||
|
||||
[rendering]
|
||||
|
||||
anti_aliasing/quality/msaa_3d=2
|
||||
textures/canvas_textures/default_texture_filter=0
|
||||
|
|
|
|||
|
|
@ -2,10 +2,21 @@
|
|||
|
||||
[ext_resource type="Script" uid="uid://bh04b3iify0hd" path="res://scripts/main.gd" id="1_main"]
|
||||
[ext_resource type="Script" uid="uid://1jimgt3d4bjj" path="res://scripts/player.gd" id="2_player"]
|
||||
[ext_resource type="Script" path="res://scripts/ground_pick.gd" id="3_ground"]
|
||||
[ext_resource type="Script" path="res://scripts/position_authority_client.gd" id="4_auth"]
|
||||
[ext_resource type="Script" path="res://scripts/interaction_request_client.gd" id="5_ix"]
|
||||
[ext_resource type="Script" path="res://scripts/interaction_radius_indicators.gd" id="6_rad"]
|
||||
[ext_resource type="Script" uid="uid://v0xpntk6uumr" path="res://scripts/ground_pick.gd" id="3_ground"]
|
||||
[ext_resource type="Script" uid="uid://ds5fkbscljkxi" path="res://scripts/position_authority_client.gd" id="4_auth"]
|
||||
[ext_resource type="Script" uid="uid://bv0xprp660hib" path="res://scripts/interaction_request_client.gd" id="5_ix"]
|
||||
[ext_resource type="Script" uid="uid://n3p4f3fky3nv" path="res://scripts/interaction_radius_indicators.gd" id="6_rad"]
|
||||
|
||||
[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)]
|
||||
geometry_collision_mask = 1
|
||||
cell_size = 0.15
|
||||
cell_height = 0.15
|
||||
border_size = 0.15
|
||||
agent_height = 1.0
|
||||
agent_radius = 0.4
|
||||
agent_max_climb = 0.35
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(20, 0.2, 20)
|
||||
|
|
@ -13,20 +24,6 @@ size = Vector3(20, 0.2, 20)
|
|||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(20, 0.2, 20)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_obstacle"]
|
||||
size = Vector3(2, 2, 2)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle"]
|
||||
size = Vector3(2, 2, 2)
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_player"]
|
||||
radius = 0.4
|
||||
height = 1.0
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_player"]
|
||||
radius = 0.4
|
||||
height = 1.0
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_terminal"]
|
||||
size = Vector3(0.9, 1, 0.4)
|
||||
|
||||
|
|
@ -36,55 +33,47 @@ albedo_color = Color(0.28, 0.38, 0.45, 1)
|
|||
[sub_resource type="BoxShape3D" id="BoxShape3D_terminal"]
|
||||
size = Vector3(0.9, 1, 0.4)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_move_reject_pedestal"]
|
||||
size = Vector3(1.5, 2.5, 1.5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_move_reject_pedestal"]
|
||||
albedo_color = Color(0.92, 0.42, 0.12, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_move_reject_pedestal"]
|
||||
size = Vector3(1.5, 2.5, 1.5)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_move_reject_farpad"]
|
||||
size = Vector3(2.5, 0.12, 2.5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_move_reject_farpad"]
|
||||
albedo_color = Color(0.32, 0.38, 0.55, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_move_reject_farpad"]
|
||||
size = Vector3(2.5, 0.12, 2.5)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_obstacle"]
|
||||
size = Vector3(2, 2, 2)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle"]
|
||||
size = Vector3(2, 2, 2)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_marker"]
|
||||
size = Vector3(0.22, 0.35, 0.22)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_marker_base"]
|
||||
albedo_color = Color(0.15, 0.4, 1, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ns19_bump_a"]
|
||||
size = Vector3(1, 0.15, 1)
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_player"]
|
||||
radius = 0.4
|
||||
height = 1.0
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ns19_bump_b"]
|
||||
size = Vector3(0.85, 0.12, 0.85)
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_player"]
|
||||
radius = 0.4
|
||||
height = 1.0
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="Convex_ns19a_frustum"]
|
||||
points = PackedVector3Array(-0.72, -0.075, -0.72, 0.72, -0.075, -0.72, 0.72, -0.075, 0.72, -0.72, -0.075, 0.72, -0.5, 0.075, -0.5, 0.5, 0.075, -0.5, 0.5, 0.075, 0.5, -0.5, 0.075, 0.5)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="Convex_ns19b_frustum"]
|
||||
points = PackedVector3Array(-0.575, -0.06, -0.575, 0.575, -0.06, -0.575, 0.575, -0.06, 0.575, -0.575, -0.06, 0.575, -0.425, 0.06, -0.425, 0.425, 0.06, -0.425, 0.425, 0.06, 0.425, -0.425, 0.06, 0.425)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ns19_pedestal"]
|
||||
size = Vector3(1.5, 2.5, 1.5)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ns19_pedestal"]
|
||||
size = Vector3(1.5, 2.5, 1.5)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ns19_farpad"]
|
||||
size = Vector3(2.5, 0.12, 2.5)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ns19_farpad"]
|
||||
size = Vector3(2.5, 0.12, 2.5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_ns19_bump"]
|
||||
albedo_color = Color(0.22, 0.52, 0.32, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_ns19_pedestal"]
|
||||
albedo_color = Color(0.92, 0.42, 0.12, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_ns19_farpad"]
|
||||
albedo_color = Color(0.32, 0.38, 0.55, 1)
|
||||
|
||||
[sub_resource type="NavigationMesh" id="NavigationMesh_district"]
|
||||
geometry_parsed_geometry_type = 2
|
||||
geometry_collision_mask = 1
|
||||
geometry_source_geometry_mode = 0
|
||||
cell_size = 0.15
|
||||
cell_height = 0.15
|
||||
agent_radius = 0.4
|
||||
agent_height = 1.0
|
||||
agent_max_climb = 0.35
|
||||
border_size = 0.15
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_player_capsule"]
|
||||
specular_mode = 2
|
||||
albedo_color = Color(0.42, 0.52, 0.7, 1)
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1358372723]
|
||||
script = ExtResource("1_main")
|
||||
|
|
@ -93,6 +82,7 @@ script = ExtResource("1_main")
|
|||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="World" unique_id=201819776]
|
||||
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 6, 0)
|
||||
light_specular = 0.0
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="World" unique_id=1124088856]
|
||||
|
|
@ -104,7 +94,6 @@ fov = 50.0
|
|||
navigation_mesh = SubResource("NavigationMesh_district")
|
||||
|
||||
[node name="Floor" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1800743112 groups=["walkable"]]
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/Floor" unique_id=152652175]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0)
|
||||
|
|
@ -116,7 +105,6 @@ shape = SubResource("BoxShape3D_floor")
|
|||
|
||||
[node name="PrototypeTerminal" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1700001 groups=["walkable"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/PrototypeTerminal" unique_id=1700002]
|
||||
mesh = SubResource("BoxMesh_terminal")
|
||||
|
|
@ -125,6 +113,39 @@ surface_material_override/0 = SubResource("Mat_terminal")
|
|||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/PrototypeTerminal" unique_id=1700006]
|
||||
shape = SubResource("BoxShape3D_terminal")
|
||||
|
||||
[node name="MoveRejectPedestal" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900021 groups=["walkable"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.5, 0, -6.5)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/MoveRejectPedestal" unique_id=1900022]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0)
|
||||
mesh = SubResource("BoxMesh_move_reject_pedestal")
|
||||
surface_material_override/0 = SubResource("Mat_move_reject_pedestal")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/MoveRejectPedestal" unique_id=1900023]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0)
|
||||
shape = SubResource("BoxShape3D_move_reject_pedestal")
|
||||
|
||||
[node name="MoveRejectFarPad" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900031 groups=["walkable"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 0.06, 9)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/MoveRejectFarPad" unique_id=1900032]
|
||||
mesh = SubResource("BoxMesh_move_reject_farpad")
|
||||
surface_material_override/0 = SubResource("Mat_move_reject_farpad")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/MoveRejectFarPad" unique_id=1900033]
|
||||
shape = SubResource("BoxShape3D_move_reject_farpad")
|
||||
|
||||
[node name="Obstacle" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1638845763]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 5)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/Obstacle" unique_id=750473628]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
||||
mesh = SubResource("BoxMesh_obstacle")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/Obstacle" unique_id=1344302688]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
||||
shape = SubResource("BoxShape3D_obstacle")
|
||||
|
||||
[node name="InteractionMarkers" type="Node3D" parent="World" unique_id=1700003]
|
||||
script = ExtResource("6_rad")
|
||||
|
||||
|
|
@ -138,86 +159,27 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.95, 0.22, -1.05)
|
|||
mesh = SubResource("BoxMesh_marker")
|
||||
surface_material_override/0 = SubResource("Mat_marker_base")
|
||||
|
||||
[node name="NS19BumpA" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900001 groups=["walkable", "ns19_bump"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.4, 0.075, -2.6)
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/NS19BumpA" unique_id=1900002]
|
||||
mesh = SubResource("BoxMesh_ns19_bump_a")
|
||||
surface_material_override/0 = SubResource("Mat_ns19_bump")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/NS19BumpA" unique_id=1900003]
|
||||
shape = SubResource("Convex_ns19a_frustum")
|
||||
|
||||
[node name="NS19BumpB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900011 groups=["walkable", "ns19_bump"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.85, 0.06, -4.85)
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/NS19BumpB" unique_id=1900012]
|
||||
mesh = SubResource("BoxMesh_ns19_bump_b")
|
||||
surface_material_override/0 = SubResource("Mat_ns19_bump")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/NS19BumpB" unique_id=1900013]
|
||||
shape = SubResource("Convex_ns19b_frustum")
|
||||
|
||||
[node name="NS19RejectPedestal" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900021 groups=["walkable"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.5, 0, -6.5)
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/NS19RejectPedestal" unique_id=1900022]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0)
|
||||
mesh = SubResource("BoxMesh_ns19_pedestal")
|
||||
surface_material_override/0 = SubResource("Mat_ns19_pedestal")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/NS19RejectPedestal" unique_id=1900023]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0)
|
||||
shape = SubResource("BoxShape3D_ns19_pedestal")
|
||||
|
||||
[node name="NS19FarPad" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1900031 groups=["walkable"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 0.06, 9)
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/NS19FarPad" unique_id=1900032]
|
||||
mesh = SubResource("BoxMesh_ns19_farpad")
|
||||
surface_material_override/0 = SubResource("Mat_ns19_farpad")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/NS19FarPad" unique_id=1900033]
|
||||
shape = SubResource("BoxShape3D_ns19_farpad")
|
||||
|
||||
[node name="Obstacle" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=1638845763]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 5)
|
||||
collision_layer = 1
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/Obstacle" unique_id=750473628]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
||||
mesh = SubResource("BoxMesh_obstacle")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/Obstacle" unique_id=1344302688]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
||||
shape = SubResource("BoxShape3D_obstacle")
|
||||
|
||||
[node name="Player" type="CharacterBody3D" parent="World/NavigationRegion3D" unique_id=352931696]
|
||||
[node name="Player" type="CharacterBody3D" parent="World" unique_id=352931696]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0.9, -5)
|
||||
collision_layer = 2
|
||||
collision_mask = 1
|
||||
floor_max_angle = 0.872665
|
||||
floor_snap_length = 0.28
|
||||
safe_margin = 0.045
|
||||
floor_snap_length = 0.22
|
||||
safe_margin = 0.055
|
||||
script = ExtResource("2_player")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/NavigationRegion3D/Player" unique_id=1695755590]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Player" unique_id=1695755590]
|
||||
shape = SubResource("CapsuleShape3D_player")
|
||||
|
||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="World/NavigationRegion3D/Player" unique_id=8880002]
|
||||
avoidance_enabled = false
|
||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="World/Player" unique_id=8880002]
|
||||
path_desired_distance = 0.35
|
||||
target_desired_distance = 0.35
|
||||
path_height_offset = 0.0
|
||||
radius = 0.4
|
||||
height = 1.0
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/Player" unique_id=2027034386]
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Player" unique_id=2027034386]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.034, 0)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("CapsuleMesh_player")
|
||||
surface_material_override/0 = SubResource("Mat_player_capsule")
|
||||
|
||||
[node name="GroundPick" type="Node3D" parent="." unique_id=2500001]
|
||||
script = ExtResource("3_ground")
|
||||
|
|
@ -232,7 +194,6 @@ script = ExtResource("5_ix")
|
|||
|
||||
[node name="MoveRejectLabel" type="Label" parent="UICanvas" unique_id=9000002]
|
||||
anchors_preset = 12
|
||||
anchor_left = 0.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ extends Node3D
|
|||
## NS-16: composes ground pick + server authority; see `ground_pick.gd` /
|
||||
## `position_authority_client.gd`. NS-18: interaction POST + radius glow preview (see child nodes).
|
||||
## NS-23: bakes `NavigationRegion3D` on startup; boot snap vs nav goal from authority signal.
|
||||
## Prototype: two **random short bumps** (sibling `StaticBody3D` under `NavigationRegion3D`; see `random_floor_bumps.gd`) **before** nav bake.
|
||||
|
||||
const MOVE_REJECT_MSG_SECONDS: float = 4.0
|
||||
|
||||
|
|
@ -11,15 +12,18 @@ var _move_reject_msg_token: int = 0
|
|||
|
||||
@onready var _camera: Camera3D = $World/Camera3D
|
||||
@onready var _nav_region: NavigationRegion3D = $World/NavigationRegion3D
|
||||
@onready var _player: CharacterBody3D = $World/NavigationRegion3D/Player
|
||||
@onready var _player: CharacterBody3D = $World/Player
|
||||
@onready var _ground_pick: Node3D = $GroundPick
|
||||
@onready var _authority: Node = $PositionAuthorityClient
|
||||
@onready var _radius_preview: Node3D = $World/InteractionMarkers
|
||||
@onready var _move_reject_label: Label = $UICanvas/MoveRejectLabel
|
||||
@onready var _floor: StaticBody3D = $World/NavigationRegion3D/Floor
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
# Runtime load + .call avoids class_name / preload static resolution issues across Godot parses.
|
||||
load("res://scripts/random_floor_bumps.gd").call("spawn_short_random_bumps", _floor)
|
||||
# Default `bake_navigation_mesh()` bakes on a background thread; sync bake ensures the map
|
||||
# exists before any `NavigationAgent3D` queries (see Godot navigation_using_navigationagents).
|
||||
_nav_region.bake_navigation_mesh(false)
|
||||
|
|
|
|||
|
|
@ -1,44 +1,94 @@
|
|||
extends CharacterBody3D
|
||||
|
||||
## NS-23: Follow server move target on walkable geometry.
|
||||
## NS-24: Idle — **`velocity = 0`**, **`floor_snap_length = FLOOR_SNAP_IDLE`** (~**11 cm**), **`move_and_slide()`**
|
||||
## only. (Post-slide **ray Y lerp** was removed: on bump **rims** hit counts / spread can cross thresholds
|
||||
## frame-to-frame and **fight** the solver.) Walking snap **`FLOOR_SNAP_MOVING`**. **`snap_to_server`** calls
|
||||
## **`reset_physics_interpolation()`** when **`physics/common/physics_interpolation`** is on.
|
||||
## **`project.godot`:** **`physics/3d/physics_engine`** = **Jolt Physics**; **`physics/common/physics_interpolation`**
|
||||
## = **off** — when **on**, micro **position** changes on bump **rims** blend across frames and read as **ghosting / jitter**.
|
||||
## **`Player`** under **`World`**.
|
||||
## **Rendering (main.tscn / project):** curved default capsule + directional light → **specular highlight**
|
||||
## and **floor z-fight** often read as “jitter” even when `global_position` is stable — player uses
|
||||
## **no-specular** material, capsule mesh **Y** lift vs **`CapsuleShape3D`** (**z-fight** vs walkables), **no cast shadow** on mesh, **MSAA 3D**, **`light_specular=0`**.
|
||||
## **Timing:** **120** physics TPS. Jitter that vanishes at **2× run speed** points at **render vs physics** cadence.
|
||||
##
|
||||
## Horizontal velocity toward nav waypoint or goal only; `velocity.y` stays 0. Height along ramps
|
||||
## and steps comes from `move_and_slide` + floor_* on CharacterBody3D. Move **legality** (step,
|
||||
## distance, bounds) is server MoveCommand only — not reimplemented here.
|
||||
## Horizontal velocity toward nav waypoint or goal only; `velocity.y` stays 0 while walking.
|
||||
## Move **legality** is server-only.
|
||||
##
|
||||
## Nav detours except when goal Y is below us: then xz toward goal (waypoints sit under bump rims).
|
||||
##
|
||||
## Idle: skip `move_and_slide()` when on floor and velocity is zero — repeated slide + floor snap
|
||||
## causes visible micro-vibration at rest.
|
||||
## **Upright + floor angle (NS-24):** **`_snap_capsule_upright()`** each tick after motion — **`Transform3D(Basis.IDENTITY, pos)`**
|
||||
## so the capsule cannot **bank** on rim contacts. **`floor_max_angle`** is **looser while walking**; **stricter when idle** on
|
||||
## flat floor; **`_floor_angle_loose_ticks`** after a walk **stops**. **Rim / straddle idle:** **moving** `floor_max_angle` when
|
||||
## **`get_floor_normal()`** is **shallow** or **`_idle_ridged_floor_contacts()`** (slab + cylinder **edge**). After idle slide,
|
||||
## **`_maybe_idle_rim_settle_nudge()`** + **`_maybe_idle_bump_proximity_escape()`** (uses **`random_floor_bump_mesh`** group mesh radius vs
|
||||
## **`random_floor_bump_collision_constants.gd`** collision lip, including **cylinder wall** hug). Do **not** reset **`global_transform`** in **`_process`** — that can **desync**
|
||||
## **`CharacterBody3D`** from the physics/render path and read as **ghosting**. **`NavigationAgent3D.avoidance_enabled`** = **false**.
|
||||
## **`_try_walk_step_assist()`** — **Y nudge** when the nav goal is **higher**, **`test_move`** says there is **headroom**,
|
||||
## we have **support** (floor or slide normal with upward component), a **wall-ish** slide opposes **XZ** toward the goal,
|
||||
## and **horizontal motion** is **not** clearly advancing. A **second** **`move_and_slide()`** runs after a nudge to fix overlap
|
||||
## (avoids “floating” off the bump edge).
|
||||
|
||||
const MOVE_SPEED: float = 5.0
|
||||
## `is_on_floor` / snap: **moving** — seam / curved-side transitions onto NS-19 bumps (~original 50°).
|
||||
const FLOOR_MAX_ANGLE_MOVING_DEG: float = 50.0
|
||||
## **Idle** — tighter to reject **vertex / vertical-face** flicker when stably at rest.
|
||||
const FLOOR_MAX_ANGLE_IDLE_DEG: float = 35.0
|
||||
## Physics ticks to keep **moving** floor angle after **`_has_walk_goal`** becomes false (arrival / clear).
|
||||
const FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP: int = 96
|
||||
const ARRIVE_EPS: float = 0.35
|
||||
const VERT_ARRIVE_EPS: float = 0.055
|
||||
const DIRECT_APPROACH_RADIUS: float = 0.85
|
||||
## Descend bypass: compares auth goal Y (often floor **surface** from pick) to `global_position.y`
|
||||
## (**mid-capsule**). True for most floor clicks — smooth bumps; skips nav (bee-line xz), so
|
||||
## obstacles may need multi-click. Nav waypoints under bump rims gave almost no horizontal speed.
|
||||
const FLOOR_SNAP_MOVING: float = 0.32
|
||||
## Idle snap — slightly **strong** to pin capsule on **rim** contacts without a second slide pass.
|
||||
const FLOOR_SNAP_IDLE: float = 0.11
|
||||
## Idle on a **rim**: if **`get_floor_normal().y`** dot world up is **below** this, use **moving** `floor_max_angle` for that tick.
|
||||
const IDLE_RIM_MIN_FLOOR_UP_DOT: float = 0.968
|
||||
## Per-tick horizontal pull when idle on a **rim** or **straddling** slab + bump (see **`_maybe_idle_rim_settle_nudge`**).
|
||||
const IDLE_RIM_SETTLE_STEP: float = 0.004
|
||||
const DESCEND_GOAL_Y_MARGIN: float = 0.06
|
||||
## Per-tick lift when **stuck** on a low vertical face while the nav goal is **higher** (NS-19 bump height ~0.15 m).
|
||||
const WALK_STEP_ASSIST_DELTA: float = 0.11
|
||||
## If **`vel_h.dot(want) >` this**, we are making enough XZ progress — do not assist (corners may still slide slowly).
|
||||
const WALK_STEP_ASSIST_MAX_FORWARD_DOT: float = 0.52
|
||||
const WALK_STEP_ASSIST_COOLDOWN_TICKS: int = 8
|
||||
## Capsule **`height`** in **`main.tscn`** (**1.0**) → origin at **center**; feet ≈ **`y - 0.5`**.
|
||||
const CAPSULE_HALF_HEIGHT: float = 0.5
|
||||
const _random_floor_bump_collision: Script = preload(
|
||||
"res://scripts/random_floor_bump_collision_constants.gd"
|
||||
)
|
||||
## Idle-only radial push off bump **lip** / **wall** (m/tick at **120** TPS).
|
||||
const IDLE_BUMP_ESCAPE_STEP: float = 0.014
|
||||
## Player **`CapsuleShape3D.radius`** in **`main.tscn`** — wall contact can occur with axis distance **>** **`col_r`**.
|
||||
const PLAYER_CAPSULE_RADIUS: float = 0.4
|
||||
|
||||
var _has_walk_goal: bool = false
|
||||
var _auth_walk_goal: Vector3 = Vector3.ZERO
|
||||
var _floor_angle_loose_ticks: int = 0
|
||||
var _step_assist_cooldown: int = 0
|
||||
|
||||
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
|
||||
_nav_agent.avoidance_enabled = false
|
||||
_nav_agent.set_target_position(global_position)
|
||||
floor_block_on_wall = true
|
||||
|
||||
|
||||
func set_authoritative_nav_goal(world_pos: Vector3) -> void:
|
||||
_auth_walk_goal = world_pos
|
||||
_has_walk_goal = true
|
||||
_floor_angle_loose_ticks = 0
|
||||
_nav_agent.set_target_position(world_pos)
|
||||
|
||||
|
||||
func clear_nav_goal() -> void:
|
||||
velocity = Vector3.ZERO
|
||||
_has_walk_goal = false
|
||||
_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
|
||||
_nav_agent.set_target_position(global_position)
|
||||
|
||||
|
||||
|
|
@ -46,7 +96,83 @@ func snap_to_server(world_pos: Vector3) -> void:
|
|||
global_position = world_pos
|
||||
velocity = Vector3.ZERO
|
||||
_has_walk_goal = false
|
||||
_floor_angle_loose_ticks = 0
|
||||
_step_assist_cooldown = 0
|
||||
_nav_agent.set_target_position(world_pos)
|
||||
reset_physics_interpolation()
|
||||
|
||||
|
||||
func _step_assist_has_support() -> bool:
|
||||
if is_on_floor():
|
||||
return true
|
||||
for i: int in get_slide_collision_count():
|
||||
if get_slide_collision(i).get_normal().y > 0.35:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool:
|
||||
if is_on_wall():
|
||||
return true
|
||||
var w2 := Vector2(want_dir_xz.x, want_dir_xz.z)
|
||||
if w2.length_squared() < 1e-8:
|
||||
return false
|
||||
w2 = w2.normalized()
|
||||
for i: int in get_slide_collision_count():
|
||||
var n: Vector3 = get_slide_collision(i).get_normal()
|
||||
if n.y > 0.52:
|
||||
continue
|
||||
var n2 := Vector2(n.x, n.z)
|
||||
if n2.length_squared() < 1e-8:
|
||||
continue
|
||||
n2 = n2.normalized()
|
||||
if n2.dot(w2) < -0.04:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _step_assist_can_raise_by(dy: float) -> bool:
|
||||
if dy <= 0.0001:
|
||||
return false
|
||||
return not test_move(global_transform, Vector3(0.0, dy, 0.0), null, safe_margin)
|
||||
|
||||
|
||||
func _try_walk_step_assist() -> bool:
|
||||
if not _has_walk_goal:
|
||||
return false
|
||||
if _auth_walk_goal.y < global_position.y + 0.025:
|
||||
return false
|
||||
if not _step_assist_has_support():
|
||||
return false
|
||||
var pos: Vector3 = global_position
|
||||
var to_h: Vector3 = Vector3(_auth_walk_goal.x - pos.x, 0.0, _auth_walk_goal.z - pos.z)
|
||||
if to_h.length_squared() < 0.03:
|
||||
return false
|
||||
var want: Vector3 = to_h.normalized()
|
||||
if not _step_assist_wallish_blocks(want):
|
||||
return false
|
||||
var vel_h: Vector3 = Vector3(velocity.x, 0.0, velocity.z)
|
||||
if vel_h.dot(want) > WALK_STEP_ASSIST_MAX_FORWARD_DOT:
|
||||
return false
|
||||
var lift: float = minf(WALK_STEP_ASSIST_DELTA, _auth_walk_goal.y - global_position.y + 0.45)
|
||||
lift = minf(lift, maxf(0.0, _auth_walk_goal.y + 0.55 - global_position.y))
|
||||
if lift <= 0.002:
|
||||
return false
|
||||
if not _step_assist_can_raise_by(lift):
|
||||
return false
|
||||
global_position.y += lift
|
||||
return true
|
||||
|
||||
|
||||
func _after_walk_move_and_slide() -> void:
|
||||
if _step_assist_cooldown > 0:
|
||||
_step_assist_cooldown -= 1
|
||||
if _step_assist_cooldown > 0:
|
||||
return
|
||||
if _try_walk_step_assist():
|
||||
_step_assist_cooldown = WALK_STEP_ASSIST_COOLDOWN_TICKS
|
||||
floor_snap_length = FLOOR_SNAP_MOVING
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func _set_horizontal_velocity_toward(point: Vector3) -> void:
|
||||
|
|
@ -58,18 +184,133 @@ func _set_horizontal_velocity_toward(point: Vector3) -> void:
|
|||
velocity.y = 0.0
|
||||
|
||||
|
||||
## `is_on_floor()` comes from the last `move_and_slide()`. Skipping slide while idle reuses that
|
||||
## stale floor state — OK for static floors; revisit if walkable geometry moves.
|
||||
func _skip_move_when_idle_on_floor() -> bool:
|
||||
return is_on_floor() and velocity.length_squared() < 1e-12
|
||||
func _physics_idle_tick() -> void:
|
||||
velocity = Vector3.ZERO
|
||||
floor_snap_length = FLOOR_SNAP_IDLE
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func _idle_ridged_floor_contacts() -> bool:
|
||||
var upish := 0
|
||||
var wallish := 0
|
||||
for i: int in get_slide_collision_count():
|
||||
var ny: float = get_slide_collision(i).get_normal().y
|
||||
if ny > 0.65:
|
||||
upish += 1
|
||||
elif ny < 0.45:
|
||||
wallish += 1
|
||||
return upish >= 1 and wallish >= 1
|
||||
|
||||
|
||||
func _maybe_idle_rim_settle_nudge() -> void:
|
||||
if get_slide_collision_count() < 1:
|
||||
return
|
||||
var shallow_floor: bool = (
|
||||
is_on_floor()
|
||||
and get_floor_normal().dot(Vector3.UP) < IDLE_RIM_MIN_FLOOR_UP_DOT
|
||||
)
|
||||
if not shallow_floor and not _idle_ridged_floor_contacts():
|
||||
return
|
||||
var h := Vector3.ZERO
|
||||
if shallow_floor:
|
||||
h = Vector3(get_floor_normal().x, 0.0, get_floor_normal().z)
|
||||
if h.length_squared() < 1e-12:
|
||||
for i: int in get_slide_collision_count():
|
||||
var n: Vector3 = get_slide_collision(i).get_normal()
|
||||
if n.y < 0.5:
|
||||
var nh: Vector3 = Vector3(n.x, 0.0, n.z)
|
||||
if nh.length_squared() > h.length_squared():
|
||||
h = nh
|
||||
if h.length_squared() < 1e-12:
|
||||
return
|
||||
var step: Vector3 = -h.normalized() * IDLE_RIM_SETTLE_STEP
|
||||
if test_move(global_transform, step, null, safe_margin):
|
||||
return
|
||||
global_position += step
|
||||
|
||||
|
||||
func _maybe_idle_bump_proximity_escape() -> void:
|
||||
var feet_y: float = global_position.y - CAPSULE_HALF_HEIGHT
|
||||
for node: Node in get_tree().get_nodes_in_group(
|
||||
_random_floor_bump_collision.RANDOM_FLOOR_BUMP_MESH_GROUP
|
||||
):
|
||||
if not node is MeshInstance3D:
|
||||
continue
|
||||
var mi := node as MeshInstance3D
|
||||
var mesh: Mesh = mi.mesh
|
||||
if not mesh is CylinderMesh:
|
||||
continue
|
||||
var cyl := mesh as CylinderMesh
|
||||
var c: Vector3 = mi.global_position
|
||||
var r_mesh: float = cyl.top_radius
|
||||
var h: float = cyl.height
|
||||
var col_r: float = minf(
|
||||
r_mesh + _random_floor_bump_collision.COLLISION_RADIUS_EXTRA,
|
||||
_random_floor_bump_collision.COLLISION_RADIUS_MAX
|
||||
)
|
||||
var dx: float = global_position.x - c.x
|
||||
var dz: float = global_position.z - c.z
|
||||
var d: float = sqrt(dx * dx + dz * dz)
|
||||
if d < 1e-5:
|
||||
continue
|
||||
var bottom_y: float = c.y - h * 0.5
|
||||
var top_y: float = c.y + h * 0.5
|
||||
if feet_y < bottom_y - 0.22 or feet_y > top_y + 0.4:
|
||||
continue
|
||||
var radial: Vector3 = Vector3(dx / d, 0.0, dz / d)
|
||||
var away: Vector3 = radial * IDLE_BUMP_ESCAPE_STEP
|
||||
var wall_band_outer: float = col_r + PLAYER_CAPSULE_RADIUS + 0.22
|
||||
# Outside visual disc but inside fat collision — clear the invisible lip.
|
||||
if d > r_mesh * 0.9 and d < col_r + 0.32:
|
||||
if not test_move(global_transform, away, null, safe_margin):
|
||||
global_position += away
|
||||
return
|
||||
# On / above the disc, hugging the **visual** rim — step onto open slab.
|
||||
if feet_y >= bottom_y - 0.06 and feet_y <= top_y + 0.1 and d >= r_mesh * 0.86 and d <= r_mesh + 0.14:
|
||||
if not test_move(global_transform, away, null, safe_margin):
|
||||
global_position += away
|
||||
return
|
||||
# Floor beside **vertical** cylinder side: axis distance can exceed **`col_r`** while capsule still touches wall.
|
||||
if (
|
||||
feet_y <= top_y + 0.14
|
||||
and d > r_mesh * 0.82
|
||||
and d < wall_band_outer
|
||||
and (is_on_wall() or _idle_ridged_floor_contacts())
|
||||
):
|
||||
if not test_move(global_transform, away, null, safe_margin):
|
||||
global_position += away
|
||||
return
|
||||
|
||||
|
||||
func _snap_capsule_upright() -> void:
|
||||
# TODO(NS-24 follow-on): When the avatar has **facing yaw**, clear **pitch/roll** only — keep **Y** rotation;
|
||||
# full **`Basis.IDENTITY`** would erase look direction.
|
||||
var p: Vector3 = global_position
|
||||
global_transform = Transform3D(Basis.IDENTITY, p)
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
var use_loose_floor_angle: bool = (
|
||||
_has_walk_goal or _floor_angle_loose_ticks > 0
|
||||
)
|
||||
if not _has_walk_goal and (
|
||||
(
|
||||
is_on_floor()
|
||||
and get_floor_normal().dot(Vector3.UP) < IDLE_RIM_MIN_FLOOR_UP_DOT
|
||||
)
|
||||
or _idle_ridged_floor_contacts()
|
||||
):
|
||||
use_loose_floor_angle = true
|
||||
floor_max_angle = deg_to_rad(
|
||||
FLOOR_MAX_ANGLE_MOVING_DEG if use_loose_floor_angle else FLOOR_MAX_ANGLE_IDLE_DEG
|
||||
)
|
||||
if not _has_walk_goal:
|
||||
velocity = Vector3.ZERO
|
||||
if _skip_move_when_idle_on_floor():
|
||||
return
|
||||
move_and_slide()
|
||||
_physics_idle_tick()
|
||||
_maybe_idle_rim_settle_nudge()
|
||||
_maybe_idle_bump_proximity_escape()
|
||||
if _floor_angle_loose_ticks > 0:
|
||||
_floor_angle_loose_ticks -= 1
|
||||
_snap_capsule_upright()
|
||||
return
|
||||
|
||||
var full_to_goal: Vector3 = _auth_walk_goal - global_position
|
||||
|
|
@ -78,21 +319,29 @@ func _physics_process(_delta: float) -> void:
|
|||
if horiz_dist <= ARRIVE_EPS and vert_err <= VERT_ARRIVE_EPS:
|
||||
velocity = Vector3.ZERO
|
||||
_has_walk_goal = false
|
||||
_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
|
||||
_nav_agent.set_target_position(global_position)
|
||||
if _skip_move_when_idle_on_floor():
|
||||
return
|
||||
move_and_slide()
|
||||
_physics_idle_tick()
|
||||
_maybe_idle_rim_settle_nudge()
|
||||
_maybe_idle_bump_proximity_escape()
|
||||
_snap_capsule_upright()
|
||||
return
|
||||
|
||||
var nav_map: RID = _nav_agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(nav_map) == 0:
|
||||
_set_horizontal_velocity_toward(_auth_walk_goal)
|
||||
floor_snap_length = FLOOR_SNAP_MOVING
|
||||
move_and_slide()
|
||||
_after_walk_move_and_slide()
|
||||
_snap_capsule_upright()
|
||||
return
|
||||
|
||||
if _auth_walk_goal.y < global_position.y - DESCEND_GOAL_Y_MARGIN:
|
||||
_set_horizontal_velocity_toward(_auth_walk_goal)
|
||||
floor_snap_length = FLOOR_SNAP_MOVING
|
||||
move_and_slide()
|
||||
_after_walk_move_and_slide()
|
||||
_snap_capsule_upright()
|
||||
return
|
||||
|
||||
if horiz_dist <= DIRECT_APPROACH_RADIUS:
|
||||
|
|
@ -107,4 +356,7 @@ func _physics_process(_delta: float) -> void:
|
|||
else:
|
||||
_set_horizontal_velocity_toward(_auth_walk_goal)
|
||||
|
||||
floor_snap_length = FLOOR_SNAP_MOVING
|
||||
move_and_slide()
|
||||
_after_walk_move_and_slide()
|
||||
_snap_capsule_upright()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
extends RefCounted
|
||||
|
||||
## Shared **`CylinderShape3D.radius`** fudge vs mesh for procedural **random floor QA bumps** (`random_floor_bumps.gd`).
|
||||
## Used by **`player.gd`** (idle lip escape). Change here only.
|
||||
## (Movement/step validation UX was originally tracked in Jira **NS-19** — not part of this script’s name.)
|
||||
|
||||
## **`MeshInstance3D.add_to_group`** for spawned bumps; **`player.gd`** queries this for proximity escape.
|
||||
const RANDOM_FLOOR_BUMP_MESH_GROUP: String = "random_floor_bump_mesh"
|
||||
|
||||
const COLLISION_RADIUS_EXTRA: float = 0.105
|
||||
const COLLISION_RADIUS_MAX: float = 0.82
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://x4bbwq1c8xe4
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
extends RefCounted
|
||||
|
||||
## Prototype QA: two short **cylinder** bumps, each on its own **`StaticBody3D`** **sibling** of **`Floor`** (same parent:
|
||||
## **`NavigationRegion3D`**). Avoids **compound internal edges** (floor box + cylinder on one body), which caused strong
|
||||
## idle jitter beside the bump. Meshes use group **`random_floor_bump_mesh`** (see **`random_floor_bump_collision_constants.gd`**). Loaded via **`main.gd`** **`load().call`**.
|
||||
## **Collision radius > mesh radius** (lip before visual rim); values in **`random_floor_bump_collision_constants.gd`**. Spawn **before** `NavigationRegion3D.bake_navigation_mesh()`.
|
||||
|
||||
const _random_floor_bump_collision: Script = preload(
|
||||
"res://scripts/random_floor_bump_collision_constants.gd"
|
||||
)
|
||||
|
||||
const COUNT: int = 2
|
||||
const HEIGHT_MIN: float = 0.08
|
||||
const HEIGHT_MAX: float = 0.14
|
||||
const RADIUS_MIN: float = 0.36
|
||||
const RADIUS_MAX: float = 0.62
|
||||
const FLOOR_SINK: float = 0.008
|
||||
const XZ_RANGE: float = 8.15
|
||||
const MIN_PAIR_SEPARATION: float = 2.85
|
||||
const MAX_PLACE_ATTEMPTS: int = 64
|
||||
const EXCLUSION_MARGIN: float = 0.72
|
||||
|
||||
static func spawn_short_random_bumps(floor_node: StaticBody3D) -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.22, 0.52, 0.32, 1.0)
|
||||
mat.roughness = 1.0
|
||||
mat.metallic = 0.0
|
||||
mat.specular_mode = 2
|
||||
|
||||
var placed_xz: Array[Vector2] = []
|
||||
for idx: int in COUNT:
|
||||
var xz: Vector2 = Vector2.ZERO
|
||||
var ok := false
|
||||
for _a: int in MAX_PLACE_ATTEMPTS:
|
||||
xz = Vector2(
|
||||
rng.randf_range(-XZ_RANGE, XZ_RANGE),
|
||||
rng.randf_range(-XZ_RANGE, XZ_RANGE)
|
||||
)
|
||||
if _xz_in_exclusion(xz):
|
||||
continue
|
||||
var far_enough := true
|
||||
for p: Vector2 in placed_xz:
|
||||
if xz.distance_to(p) < MIN_PAIR_SEPARATION:
|
||||
far_enough = false
|
||||
break
|
||||
if not far_enough:
|
||||
continue
|
||||
ok = true
|
||||
break
|
||||
if not ok:
|
||||
push_warning("RandomFloorBumps: could not place bump %d after %d tries" % [idx, MAX_PLACE_ATTEMPTS])
|
||||
continue
|
||||
placed_xz.append(xz)
|
||||
var h: float = rng.randf_range(HEIGHT_MIN, HEIGHT_MAX)
|
||||
var r: float = rng.randf_range(RADIUS_MIN, RADIUS_MAX)
|
||||
_add_cylinder_bump(floor_node, xz.x, xz.y, h, r, mat, idx)
|
||||
|
||||
|
||||
static func _xz_in_exclusion(xz: Vector2) -> bool:
|
||||
var ex: Array[Vector2] = [
|
||||
Vector2(-5.0, -5.0),
|
||||
Vector2(0.0, 0.0),
|
||||
Vector2(6.0, 5.0),
|
||||
Vector2(7.5, -6.5),
|
||||
Vector2(9.0, 9.0),
|
||||
]
|
||||
var rad: Array[float] = [2.65, 1.2, 2.25, 1.65, 1.85]
|
||||
for i: int in ex.size():
|
||||
if xz.distance_to(ex[i]) < rad[i] + EXCLUSION_MARGIN:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _add_cylinder_bump(
|
||||
floor_node: StaticBody3D,
|
||||
x: float,
|
||||
z: float,
|
||||
height: float,
|
||||
radius: float,
|
||||
mat: StandardMaterial3D,
|
||||
idx: int
|
||||
) -> void:
|
||||
var cy: float = height * 0.5 - FLOOR_SINK
|
||||
var local_center := Vector3(x, cy, z)
|
||||
var nav_parent := floor_node.get_parent()
|
||||
if nav_parent == null:
|
||||
push_error("RandomFloorBumps: Floor has no parent; cannot spawn bump")
|
||||
return
|
||||
|
||||
var bump_root := StaticBody3D.new()
|
||||
bump_root.name = "RandomTestBump%d" % idx
|
||||
bump_root.add_to_group("walkable")
|
||||
bump_root.collision_layer = floor_node.collision_layer
|
||||
bump_root.collision_mask = floor_node.collision_mask
|
||||
nav_parent.add_child(bump_root)
|
||||
bump_root.global_position = floor_node.to_global(local_center)
|
||||
|
||||
var mesh_inst := MeshInstance3D.new()
|
||||
mesh_inst.name = "RandomTestBump%d_mesh" % idx
|
||||
mesh_inst.add_to_group(_random_floor_bump_collision.RANDOM_FLOOR_BUMP_MESH_GROUP)
|
||||
var cm := CylinderMesh.new()
|
||||
cm.height = height
|
||||
cm.top_radius = radius
|
||||
cm.bottom_radius = radius
|
||||
cm.radial_segments = 24
|
||||
mesh_inst.mesh = cm
|
||||
mesh_inst.set_surface_override_material(0, mat)
|
||||
mesh_inst.position = Vector3.ZERO
|
||||
bump_root.add_child(mesh_inst)
|
||||
|
||||
var col := CollisionShape3D.new()
|
||||
col.name = "RandomTestBump%d_shape" % idx
|
||||
var csh := CylinderShape3D.new()
|
||||
csh.height = height
|
||||
csh.radius = minf(
|
||||
radius + _random_floor_bump_collision.COLLISION_RADIUS_EXTRA,
|
||||
_random_floor_bump_collision.COLLISION_RADIUS_MAX
|
||||
)
|
||||
col.shape = csh
|
||||
col.position = Vector3.ZERO
|
||||
bump_root.add_child(col)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dgpkx05lwefn8
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# NS-24 — Implementation plan
|
||||
|
||||
## Story reference
|
||||
|
||||
| Field | Value |
|
||||
|--------|--------|
|
||||
| **Key** | NS-24 |
|
||||
| **Title** | Godot client: eliminate CharacterBody3D idle / rest vibration |
|
||||
| **Jira** | [NS-24](https://neon-sprawl.atlassian.net/browse/NS-24) |
|
||||
| **Parent** | [NS-20 — Tech Debt](https://neon-sprawl.atlassian.net/browse/NS-20) |
|
||||
| **Related** | [NS-23](https://neon-sprawl.atlassian.net/browse/NS-23) (path-follow baseline; idle jitter explicitly deferred here) |
|
||||
|
||||
## Goal, scope, and out-of-scope
|
||||
|
||||
**Goal:** **Stable rest pose** — no perceptible micro-vibration when horizontal velocity is zero, there is no active walk goal, and the avatar stands on walkable geometry, including prototype floors and **NS-19** stepped bumps.
|
||||
|
||||
**In scope**
|
||||
|
||||
- Diagnose and fix (or materially reduce) idle jitter on the prototype **`CharacterBody3D`** player driven by **`player.gd`** (`move_and_slide`, floor interaction, **`NavigationAgent3D`** coupling as relevant).
|
||||
- Preserve **NS-23** behavior: walking, arrival at goal, **`snap_to_server`** on boot, descend bypass tradeoff, and **NS-19** validation UX on the server side (client must not regress those flows).
|
||||
- Document non-obvious fixes in **`client/README.md`** and/or the **`player.gd`** header (per Jira).
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Full prediction/reconciliation, server-side nav, or redesign of **MoveCommand** / **PositionAuthorityClient** contracts.
|
||||
- Replacing the capsule with a different locomotion stack unless a minimal inspector tweak is sufficient.
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [x] At rest (no click-to-move goal, feet on floor), **no visible vibration** in the default prototype scene for **several seconds** of observation.
|
||||
- [x] **Walking**, **arrival**, and **`snap_to_server`** still behave correctly; **no regression** on NS-19 bumps or the **NS-23** obstacle / descend-bypass tradeoff documented in **`client/README.md`**.
|
||||
- [x] If a **scene** or **project** setting change is required, it is **checked in** and called out for designers (README or short comment in scene notes).
|
||||
|
||||
## Resolution (shipped)
|
||||
|
||||
**Jolt Physics** + **`physics_interpolation` off**; QA bumps on **separate `StaticBody3D`** bodies (no floor+bump compound internal edges); idle **`floor_max_angle`** loosened on rim/straddle; **rim settle** + **`random_floor_bump_mesh`** lip/wall escape; **`NavigationAgent3D.avoidance_enabled`** false, **`floor_block_on_wall`** true; **no `global_transform` in `_process`**; **visual** capsule **mesh Y lift** vs collision to reduce **z-fight** with bump tops. Shared bump collider fudge: **`client/scripts/random_floor_bump_collision_constants.gd`**.
|
||||
|
||||
## Technical approach
|
||||
|
||||
1. **Reproduce and isolate** in Godot 4.x editor: Remote **Transform** on the player (and camera parent if suspected), **Visible Collision Shapes**, and frame / physics profiler to see whether jitter is position, floor snap, or render-only.
|
||||
2. **CharacterBody3D / physics** — iterate on documented levers: **`move_and_slide`** at idle with **`floor_snap_length = 0`**; **`safe_margin`**; compound **`Floor`** collision; **`physics/3d/physics_engine`** (**Jolt Physics** vs Godot Physics — Jolt targets internal-edge / compound-shape jitter); **`floor_stop_on_slope`**, **`motion_mode`**, **`up_direction`** if still needed.
|
||||
3. **Navigation agent** — confirm **`NavigationAgent3D`** target updates at rest (`set_target_position(global_position)` on clear/arrive) do not trigger path churn each frame; adjust only if measurements show agent-driven motion.
|
||||
4. **Camera / parenting** — rule out camera or child node noise if transform root is stable.
|
||||
5. **Ship** the smallest change that clears the AC (code, scene, or project), then update **`client/README.md`** to remove or narrow the “Known issue” idle jitter line and describe the chosen pattern.
|
||||
|
||||
**Shipped note:** Idle uses **non-zero** **`floor_snap_length`** (see **`player.gd`**) rather than zero snap from the investigation list; straddle/rim logic and bump geometry split were the main levers. Code review: **`docs/reviews/2026-04-05-NS-24.md`**.
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `client/scripts/random_floor_bump_collision_constants.gd` | Single source for QA bump **`CylinderShape3D`** radius fudge vs mesh and **`RANDOM_FLOOR_BUMP_MESH_GROUP`** (shared by **`random_floor_bumps.gd`** and **`player.gd`** lip escape). |
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `client/scripts/player.gd` | Idle **`move_and_slide`** policy, walking snap, arrival path, bump lip escape (imports **`random_floor_bump_collision_constants.gd`**). |
|
||||
| `client/scenes/main.tscn` | Player **`CharacterBody3D`** / **`NavigationAgent3D`** inspector properties, collision layers, or nav region only if diagnosis requires it (AC: designer-visible changes documented). |
|
||||
| `client/project.godot` | Physics or project defaults (e.g. physics ticks, 3D defaults) only if required and safe for the whole project. |
|
||||
| `client/README.md` | Replace **Known issue: idle / rest jitter** with the fix summary and any manual verification deltas. |
|
||||
|
||||
## Tests
|
||||
|
||||
| Action | What to cover |
|
||||
|--------|----------------|
|
||||
| **Manual (required)** | Run main scene with server as in **NS-16/NS-23** README steps: stand idle **10+ s** at spawn and after a move; click-move including **stepped bumps** (NS-19 props); cold boot **`snap_to_server`**; far/vertical rejects still show NS-19 UX; optional multi-click around **`Obstacle`** per NS-23 tradeoff. |
|
||||
| **Automated** | **None** — no Godot/GDScript test harness in repo; server **xUnit** suites unchanged unless an unrelated regression appears (not expected). |
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
**Resolved for prototype QA:** Primary causes were **compound internal edges** (mitigated by per-bump bodies), **physics/render** artifacts (**interpolation**, **`CharacterBody3D`** transform desync, **z-fight**), and **rim/wall** contacts (mitigated by idle policy + small escapes). **Remaining:** **`_snap_capsule_upright()`** uses full **identity basis** — when **yaw** facing is added, preserve **Y** rotation only (see **`TODO`** in **`player.gd`**). Moving platforms / uneven nav mesh not validated.
|
||||
|
||||
## PR / review
|
||||
|
||||
Cross-check [E1.M1 — InputAndMovementRuntime](../decomposition/modules/E1_M1_InputAndMovementRuntime.md), **`docs/plans/NS-23-implementation-plan.md`** (residual polish pointer), and **`client/README.md`** after changes.
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# Code review — NS-24 idle / rest stability (Godot client)
|
||||
|
||||
**Date:** 2026-04-05
|
||||
**Scope:** Branch `NS-24-idle-vibration-fix` vs merge-base `3283f6d04c8724f6536ca591a2a4c8ca06ea7b61` (tracked as `origin/main` at review time). Issue **NS-24**; no PR URL supplied.
|
||||
**Base:** `origin/main` @ `3283f6d04c8724f6536ca591a2a4c8ca06ea7b61`
|
||||
|
||||
**Follow-up:** Review **suggestions** below are **done** (strikethrough + **Done.**); **nits** remain optional.
|
||||
|
||||
## Verdict
|
||||
|
||||
**Approve with nits**
|
||||
|
||||
## Summary
|
||||
|
||||
This branch delivers **NS-24** by hardening the prototype **`CharacterBody3D`** idle path (`player.gd`): dual **`floor_max_angle`** behavior, rim/straddle detection, idle nudges, bump proximity escape, **`floor_block_on_wall`**, **`NavigationAgent3D.avoidance_enabled = false`**, and **physics interpolation off** at project + player node level. It fixes **compound internal-edge** issues by spawning each QA bump as its **own `StaticBody3D`** (`random_floor_bumps.gd`), adds **Jolt** in **`project.godot`**, and reduces **z-fighting** via capsule **mesh Y lift** (visual-only vs collision shape). **`client/README.md`** and **`server/README.md`** (bump height copy) are updated. **Server contracts** (`MoveCommand` / `PositionState`) are not changed; risk is localized to client feel and QA geometry. Manual QA by the author reports **no remaining jitter** in exercised poses.
|
||||
|
||||
## Documentation checked
|
||||
|
||||
| Document | Result |
|
||||
|----------|--------|
|
||||
| `docs/plans/NS-24-implementation-plan.md` | **Matches** — **Resolution**, checked AC, **Shipped note**, updated **Open questions** (`015ca13`). |
|
||||
| `docs/plans/NS-23-implementation-plan.md` | **Matches** at intent level (client nav visual, server authority unchanged); no contract drift observed in the diff. |
|
||||
| `docs/decomposition/modules/E1_M1_InputAndMovementRuntime.md` | **Matches.** Prototype client locomotion and reconciliation surface remain consistent; changes are polish under “movement-loop” / QA, not authority or wire shape. |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | **N/A** for code behavior; cited as the hub for module IDs. No register **Status** change required for this client-only jitter fix. |
|
||||
| `docs/decomposition/modules/client_server_authority.md` | **N/A** (no change to who owns `PositionState` or move validation). |
|
||||
| `client/README.md` | **Matches** NS-24 call to document the pattern and manual steps (idle 10+ s, bumps). |
|
||||
|
||||
## Blocking issues
|
||||
|
||||
*(none)*
|
||||
|
||||
## Suggestions
|
||||
|
||||
**All done.**
|
||||
|
||||
1. ~~**Close the loop in `docs/plans/NS-24-implementation-plan.md`:**~~ **Done.** Check off acceptance criteria; add **Resolution** (shipped mitigations) and **Shipped note** under technical approach — `015ca13`.
|
||||
2. ~~**DRY bump collision constants** (`BUMP_COLLISION_*` aligned across `player.gd` / `random_floor_bumps.gd`):~~ **Done.** `client/scripts/random_floor_bump_collision_constants.gd` + preload from both scripts; README references — `015ca13`, doc sync `87d8ee0` (file later renamed off **`ns19_*`** identifiers).
|
||||
3. ~~**Future character yaw** (`_snap_capsule_upright()` full identity basis):~~ **Done.** `TODO(NS-24 follow-on)` on `_snap_capsule_upright()` in `player.gd`; plan **Open questions** — `015ca13`.
|
||||
|
||||
## Nits
|
||||
|
||||
- **`get_tree().get_nodes_in_group(...)`** on **`random_floor_bump_mesh`** each idle tick is fine at **two** bumps; if the group grows, cache references after spawn or guard cost.
|
||||
- ~~**Commit history:**~~ **Done.** Branch history squashed to a **single** **`NS-24:`** commit before merge; no stray **`fix:`** / **`docs:`** subjects on the slice.
|
||||
|
||||
## Verification
|
||||
|
||||
Per **`NS-24-implementation-plan.md`** and **`client/README.md`**:
|
||||
|
||||
1. Run **`main.tscn`** with server as in NS-16/NS-23 README steps.
|
||||
2. **Idle 10+ s** at spawn and after a move (no click goal); confirm **no visible vibration** on slab and on **random green bumps** (new positions each run).
|
||||
3. **Walking**, **arrival**, **`snap_to_server`** on cold boot; **NS-19** reject UX (pedestal / far pad) unchanged.
|
||||
4. **Obstacle / descend** flows per NS-23 tradeoff (multi-click around tall obstacle if needed).
|
||||
5. Spot-check **bump rim**, **cylinder side**, and **floor–bump** contact (prior jitter loci).
|
||||
|
||||
**Automated:** None required for NS-24 (no Godot harness in repo).
|
||||
|
|
@ -74,7 +74,7 @@ Unknown player ids return **404**. Full zone sync / replication is still out of
|
|||
| `Game:MovementValidation:DistrictBoundsEnabled` | Axis-aligned box on **target** (inclusive min/max) | `false` |
|
||||
| `Game:MovementValidation:DistrictMinX` … `DistrictMaxZ` | District extents when enabled | ±12 / Y −2…24 |
|
||||
|
||||
**Manual QA (Godot `main.tscn`):** **Green bumps** (~0.15 m rise) stay under default `MaxVerticalStep` from the dev spawn; **orange reject pedestal** top is ~2.5 m above floor so a floor-level click yields **`vertical_step_exceeded`**; **far pad** is beyond default horizontal reach for **`horizontal_step_exceeded`**. The client’s **`ground_pick.gd`** only accepts ray hits whose surface **normal** is mostly **upward** (`MIN_WALKABLE_NORMAL_DOT_UP`), so **vertical faces** on the pedestal do not emit move targets—this complements server per-step limits and prevents “stair-stepping” up walls.
|
||||
**Manual QA (Godot `main.tscn`):** **Green bumps** are **two random short cylinders** each run (~**8–14 cm** rise) and stay under default `MaxVerticalStep` from the dev spawn; **orange reject pedestal** top is ~2.5 m above floor so a floor-level click yields **`vertical_step_exceeded`**; **far pad** is beyond default horizontal reach for **`horizontal_step_exceeded`**. The client’s **`ground_pick.gd`** only accepts ray hits whose surface **normal** is mostly **upward** (`MIN_WALKABLE_NORMAL_DOT_UP`), so **vertical faces** on the pedestal do not emit move targets—this complements server per-step limits and prevents “stair-stepping” up walls.
|
||||
|
||||
Request body (example):
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue