Skip to content

Commit 097f786

Browse files
committed
Fix intermittent ground friction
Treat CharacterBody3D floor contacts as grounded alongside the existing short probe, preventing map geometry from intermittently disabling friction. Add a Rooftops regression test that reproduces the mismatched floor state. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f9ed8b9d-73e0-4758-921a-658f7ec1585b
1 parent 809bd78 commit 097f786

4 files changed

Lines changed: 71 additions & 1 deletion

File tree

scripts/test-godot.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ TEST_SCRIPTS=(
1515
"res://tests/aim_trainer_smoke.gd"
1616
"res://tests/death_camera_test.gd"
1717
"res://tests/elimination_round_state_test.gd"
18+
"res://tests/player_grounding_test.gd"
1819
)
1920

2021
if ! command -v "$GODOT_BIN" >/dev/null 2>&1; then

src/player/player.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ func _apply_gravity(velocity_y: float, delta: float) -> float:
310310
return velocity_y - gravity * delta
311311

312312
func grounded() -> bool:
313-
return test_move(global_transform, Vector3(0, -0.01, 0))
313+
return is_on_floor() or test_move(global_transform, Vector3(0, -0.01, 0))
314314

315315
func _apply_friction(vel_planar: Vector2, delta: float, wish_dir: Vector2, jump_input: bool) -> Vector2:
316316
if not grounded() or jump_input:

tests/player_grounding_test.gd

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
extends SceneTree
2+
3+
const ROOFTOPS_SCENE := "res://src/maps/speedrun/map_rooftops.tscn"
4+
const TestCase = preload("res://tests/support/test_case.gd")
5+
6+
func _init() -> void:
7+
call_deferred("_run")
8+
9+
func _run() -> void:
10+
var t := TestCase.new()
11+
await _wait_for_app_context()
12+
for _frame in range(5):
13+
await process_frame
14+
15+
if current_scene != null:
16+
current_scene.free()
17+
current_scene = null
18+
await physics_frame
19+
20+
var rooftops_scene := load(ROOFTOPS_SCENE) as PackedScene
21+
t.check(rooftops_scene != null, "Rooftops should load for the grounding regression test")
22+
if rooftops_scene == null:
23+
quit(t.finish())
24+
return
25+
26+
var rooftops := rooftops_scene.instantiate()
27+
var map: Node3D = rooftops.get_node("Map")
28+
var player = rooftops.get_node("Player")
29+
rooftops.remove_child(map)
30+
rooftops.remove_child(player)
31+
rooftops.free()
32+
for child in map.get_children():
33+
if child is Area3D:
34+
if "audio_player" in child and is_instance_valid(child.audio_player):
35+
child.audio_player.free()
36+
child.free()
37+
root.add_child(map)
38+
root.add_child(player)
39+
player.setup_as_local_player()
40+
player.set_physics_process(false)
41+
player._time_since_last_run_sound = -100.0
42+
await physics_frame
43+
44+
var missed_floor_contact := false
45+
for frame in range(60):
46+
var input := Vector2(0, -1) if frame < 30 else Vector2.ZERO
47+
player._movement_process(1.0 / 60.0, input, false)
48+
if player.is_on_floor() and not player.grounded():
49+
missed_floor_contact = true
50+
await physics_frame
51+
52+
t.check(
53+
not missed_floor_contact,
54+
"Grounded state should include floor contacts reported by CharacterBody3D",
55+
)
56+
player._run_audio_player.stop()
57+
player._run_audio_player.stream = null
58+
player.free()
59+
map.free()
60+
await process_frame
61+
quit(t.finish())
62+
63+
func _wait_for_app_context() -> void:
64+
for _frame in range(600):
65+
var global_node := root.get_node_or_null("Global")
66+
if global_node != null and global_node.context != null:
67+
return
68+
await process_frame

tests/player_grounding_test.gd.uid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://d2w2ngxftm2gd

0 commit comments

Comments
 (0)