diff --git a/client/project.godot b/client/project.godot index fdae86a..f42beef 100644 --- a/client/project.godot +++ b/client/project.godot @@ -67,6 +67,7 @@ dev_toggle_occluder_obstacle={ [physics] common/physics_ticks_per_second=120 +3d/default_gravity=9.81 3d/physics_engine="Jolt Physics" [rendering] diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 38f9e49..1fbbf4d 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -126,6 +126,9 @@ var _debug_trace_frame: int = 0 var _walk_nav_column_steering: bool = false ## Schmitt latch for vertical routing (see [member WALK_VERT_ROUTE_LATCH_ON_SEP]). var _walk_vert_route_latched: bool = false +## GROUNDED [method move_and_slide] clears downward [code]velocity.y[/code] on floor; accumulate peel +## speed here and apply [code]global_position.y[/code] so terrace lips can actually drop. +var _walk_ledge_peel_vy: float = 0.0 @onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D @@ -150,11 +153,13 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void: _idle_stable_enter_streak = 0 _walk_nav_column_steering = false _walk_vert_route_latched = false + _walk_ledge_peel_vy = 0.0 _nav_agent.set_target_position(world_pos) func clear_nav_goal() -> void: velocity = Vector3.ZERO + _walk_ledge_peel_vy = 0.0 _has_walk_goal = false _floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP _step_assist_active = false @@ -195,6 +200,7 @@ func snap_to_server(world_pos: Vector3) -> void: global_position = settled velocity = Vector3.ZERO _has_walk_goal = false + _walk_ledge_peel_vy = 0.0 # Force corrective idle ticks so Jolt resolves any residual physics state before walking. _floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP _step_assist_cooldown = 0 @@ -443,30 +449,40 @@ func _walk_has_close_floor_probe_below(move_dir_xz: Vector2) -> bool: ## toward a floor click and poisons horizontal motion / slide resolution. ## Do apply gravity when [code]is_on_floor()[/code] is still true but there is no walkable surface ## within [member WALK_SUPPORT_PROBE_DEPTH] under the foot disk (ledge / void while moving). -## [param feet_y] is [method capsule_feet_y] for the current body origin (see [member CAPSULE_HALF_HEIGHT]). -## [param move_dir_xz] steers multi-sample rays (see [method _walk_probe_xz_offsets]). -func _apply_walk_air_gravity( - delta: float, feet_y: float, move_dir_xz: Vector2 = Vector2.ZERO -) -> void: - var apply_gravity: bool = not is_on_floor() - if ( - not apply_gravity - and _has_walk_goal - and is_on_floor() - and not _walk_has_close_floor_probe_below(move_dir_xz) - ): - apply_gravity = true - if ( - not apply_gravity - and _has_walk_goal - and is_on_floor() - and feet_y > _auth_walk_goal.y + WALK_DEEP_DESCENT_FEET_ABOVE_GOAL - ): - apply_gravity = true - if apply_gravity: +## Only while **airborne**. With default [member MotionMode.MOTION_MODE_GROUNDED], [method move_and_slide] +## clears downward [code]velocity.y[/code] every tick while [method is_on_floor] — adding gravity on the +## velocity does **not** move the body off a terrace lip; use [method _apply_walk_post_slide_ledge_peel]. +func _apply_walk_air_gravity(delta: float, _feet_y: float, _move_dir_xz: Vector2 = Vector2.ZERO) -> void: + if not is_on_floor(): velocity += get_gravity() * delta +## After [method move_and_slide], peel the capsule down when the authoritative goal is below the feet +## but the engine still reports floor contact (GROUNDED mode ate negative [code]velocity.y[/code]). +## Uses a small accumulated peel speed so displacement matches [code]∫ g dt[/code], not [code]g Δt[/code] (wrong units). +func _apply_walk_post_slide_ledge_peel( + delta: float, feet_y: float, move_dir_xz: Vector2 +) -> void: + if not _has_walk_goal: + _walk_ledge_peel_vy = 0.0 + return + if not is_on_floor(): + _walk_ledge_peel_vy = 0.0 + return + if _auth_walk_goal.y >= feet_y - DESCEND_GOAL_Y_MARGIN: + _walk_ledge_peel_vy = 0.0 + return + var peel: bool = ( + feet_y > _auth_walk_goal.y + WALK_DEEP_DESCENT_FEET_ABOVE_GOAL + or not _walk_has_close_floor_probe_below(move_dir_xz) + ) + if not peel: + _walk_ledge_peel_vy = 0.0 + return + _walk_ledge_peel_vy += get_gravity().y * delta + global_position.y += _walk_ledge_peel_vy * delta + + func _walk_floor_snap_length( feet_y: float, want_goal_h: Vector3, move_dir_xz: Vector2 = Vector2.ZERO ) -> float: @@ -938,6 +954,7 @@ func _physics_process(delta: float) -> void: if horiz_dist <= ARRIVE_EPS and vert_err <= VERT_ARRIVE_EPS: velocity = Vector3.ZERO _has_walk_goal = false + _walk_ledge_peel_vy = 0.0 floor_block_on_wall = true _floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP _step_assist_active = false @@ -1003,6 +1020,10 @@ func _physics_process(delta: float) -> void: _apply_walk_air_gravity(delta, feet_y, walk_move_dir_xz) floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h, walk_move_dir_xz) move_and_slide() + var feet_after: float = capsule_feet_y( + global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS + ) + _apply_walk_post_slide_ledge_peel(delta, feet_after, walk_move_dir_xz) _after_walk_move_and_slide() _clear_step_assist_after_walk_move() _debug_trace_transform("physics")