From 1ab2a291d2db7546318e7eee5865a85c84d2e3bd Mon Sep 17 00:00:00 2001 From: Voyager Date: Tue, 25 Aug 2026 00:12:01 -0400 Subject: [PATCH] fix terrain collision and water recovery --- tests/fishing_surface_validation.gd | 29 +++++++++++++++ tests/generated_world_runtime_validation.gd | 37 +++++++++++++++++++ world/generation/generated_world_region.gd | 1 + .../props/definitions/prop_palm.tres | 5 ++- .../props/definitions/prop_pine.tres | 3 +- .../props/definitions/prop_pine_large.tres | 3 +- .../props/definitions/prop_tree_1.tres | 1 + .../props/definitions/prop_tree_2.tres | 3 +- .../props/definitions/prop_tree_3.tres | 1 + .../props/definitions/prop_tree_large.tres | 3 +- world/player_water_trigger.gd | 26 ++++++++++--- world/regions/starter_island_region.tscn | 1 + world/water_body_authoring.gd | 8 ++++ 13 files changed, 110 insertions(+), 11 deletions(-) diff --git a/tests/fishing_surface_validation.gd b/tests/fishing_surface_validation.gd index eb02c6c..8248c36 100644 --- a/tests/fishing_surface_validation.gd +++ b/tests/fishing_surface_validation.gd @@ -156,6 +156,7 @@ func _validate_portable_water_body() -> void: PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN, ) water_body.set("recovery_entry_depth_threshold", 0.1) + water_body.set("recovery_entry_confirmation_seconds", 0.2) water_body.set("fish_pool", PondPool) root.add_child(water_body) await process_frame @@ -183,6 +184,34 @@ func _validate_portable_water_body() -> void: == PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN ) assert(is_equal_approx(recovery_region.entry_depth_threshold, 0.1)) + assert(is_equal_approx(recovery_region.entry_confirmation_seconds, 0.2)) + + var recovery_player := PlayerScene.instantiate() as Player + assert(recovery_player != null) + root.add_child(recovery_player) + recovery_player.set_process(false) + recovery_player.set_physics_process(false) + var recovery_requests: Array[float] = [] + recovery_region.recovery_requested.connect( + func(_player: Player, requested_surface_height: float) -> void: + recovery_requests.append(requested_surface_height) + ) + recovery_region.call("_on_body_entered", recovery_player) + recovery_player.global_position.y = 6.8 + recovery_region.call("_physics_process", 0.1) + assert(recovery_requests.is_empty()) + # Breaking the submersion resets confirmation rather than carrying a stale + # partial fall into a later jump. + recovery_player.global_position.y = 7.0 + recovery_region.call("_physics_process", 0.1) + recovery_player.global_position.y = 6.8 + recovery_region.call("_physics_process", 0.11) + assert(recovery_requests.is_empty()) + recovery_region.call("_physics_process", 0.1) + assert(recovery_requests == [7.0]) + recovery_region.call("_on_body_exited", recovery_player) + recovery_player.queue_free() + await process_frame water_body.queue_free() await process_frame diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 0143322..adec70b 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -490,6 +490,10 @@ func _validate_generated_region( == PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN ) assert(is_equal_approx(fresh_recovery.entry_depth_threshold, 0.1)) + assert(is_equal_approx( + fresh_recovery.entry_confirmation_seconds, + 0.2, + )) var coordinate_tokens := child.name.trim_prefix("FreshWater_").split("_") assert(coordinate_tokens.size() == 2) var coordinate := Vector2i( @@ -1043,6 +1047,25 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void: assert(palm.maximum_cluster_radius > palm.minimum_cluster_radius) assert(palm.prefer_ocean_facing) assert(not palm.local_overhang_direction.is_zero_approx()) + var aligned_tree_collisions: Dictionary[StringName, Vector2] = { + &"prop_tree_1": Vector2(0.08, 0.01), + &"prop_tree_2": Vector2(0.1, 0.075), + &"prop_tree_3": Vector2(-0.1, 0.18), + &"prop_tree_large": Vector2(0.084, 0.504), + &"prop_pine": Vector2(-0.044, 0.083), + &"prop_pine_large": Vector2(-0.04, 0.075), + &"prop_palm": Vector2(0.15, -0.1), + } + for tree_id: StringName in aligned_tree_collisions: + var aligned_tree := catalog.definition_for_id(tree_id) + assert(aligned_tree != null and aligned_tree.has_cylinder_collision()) + var collision_center := Vector2( + aligned_tree.collision_offset.x, + aligned_tree.collision_offset.z, + ) + assert(collision_center.is_equal_approx( + aligned_tree_collisions[tree_id] + )) var forest := region.get_biome_catalog().definition_for_id(&"biome_forest") var forest_rule := forest.prop_rule_for_group(&"grass_tree") var large_tree := catalog.definition_for_id(&"prop_tree_large") @@ -1139,6 +1162,20 @@ func _validate_decoration_transform( or collision.shape is BoxShape3D ) assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE)) + if collision.shape is CylinderShape3D: + var cylinder := collision.shape as CylinderShape3D + assert(is_equal_approx( + cylinder.radius, + definition.collision_radius * visual_scale, + )) + assert(is_equal_approx( + cylinder.height, + definition.collision_height * visual_scale, + )) + assert(collision.position.is_equal_approx( + definition.collision_offset * visual_scale + + Vector3.UP * cylinder.height * 0.5 + )) else: assert(collision == null) var query := PhysicsRayQueryParameters3D.create( diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 78188e0..8987caf 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -453,6 +453,7 @@ func _configure_fresh_water(records: Array[Dictionary]) -> void: PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN ) body.recovery_entry_depth_threshold = 0.1 + body.recovery_entry_confirmation_seconds = 0.2 body.fish_pool = _fresh_water_pool(tags) body.location_tags = _fresh_water_location_tags(tags) body.selection_priority = 10 diff --git a/world/generation/props/definitions/prop_palm.tres b/world/generation/props/definitions/prop_palm.tres index e586f3d..c8021d4 100644 --- a/world/generation/props/definitions/prop_palm.tres +++ b/world/generation/props/definitions/prop_palm.tres @@ -11,7 +11,7 @@ packed_scene = ExtResource("2_scene") procedural_group = &"sand_tree" required_chunk_tags = PackedStringArray("sand") minimum_spawn_chunk_distance = 2 -clearance_radius = 0.65 +clearance_radius = 0.75 minimum_visual_scale = 0.5 maximum_visual_scale = 1.0 minimum_cluster_size = 2 @@ -21,6 +21,7 @@ maximum_cluster_radius = 3.0 prefer_ocean_facing = true local_overhang_direction = Vector2(-0.883, 0.469) ocean_facing_spread_degrees = 55.0 -collision_radius = 0.4 +collision_radius = 0.7 collision_height = 6.0 +collision_offset = Vector3(0.15, 0, -0.1) gatherable_surface_material_names = PackedStringArray("wood_light") diff --git a/world/generation/props/definitions/prop_pine.tres b/world/generation/props/definitions/prop_pine.tres index b3224a9..5dd460a 100644 --- a/world/generation/props/definitions/prop_pine.tres +++ b/world/generation/props/definitions/prop_pine.tres @@ -15,5 +15,6 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.55 minimum_visual_scale = 0.65 maximum_visual_scale = 1.2 -collision_radius = 0.5 +collision_radius = 0.85 collision_height = 4.0 +collision_offset = Vector3(-0.044, 0, 0.083) diff --git a/world/generation/props/definitions/prop_pine_large.tres b/world/generation/props/definitions/prop_pine_large.tres index df9076d..bc70868 100644 --- a/world/generation/props/definitions/prop_pine_large.tres +++ b/world/generation/props/definitions/prop_pine_large.tres @@ -15,5 +15,6 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.65 minimum_visual_scale = 0.75 maximum_visual_scale = 1.2 -collision_radius = 0.65 +collision_radius = 0.75 collision_height = 9.5 +collision_offset = Vector3(-0.04, 0, 0.075) diff --git a/world/generation/props/definitions/prop_tree_1.tres b/world/generation/props/definitions/prop_tree_1.tres index 3553988..4688578 100644 --- a/world/generation/props/definitions/prop_tree_1.tres +++ b/world/generation/props/definitions/prop_tree_1.tres @@ -27,4 +27,5 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.4 collision_height = 3.2 +collision_offset = Vector3(0.08, 0, 0.01) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/props/definitions/prop_tree_2.tres b/world/generation/props/definitions/prop_tree_2.tres index 868e044..a6e7895 100644 --- a/world/generation/props/definitions/prop_tree_2.tres +++ b/world/generation/props/definitions/prop_tree_2.tres @@ -25,6 +25,7 @@ variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) -collision_radius = 0.5 +collision_radius = 0.55 collision_height = 3.8 +collision_offset = Vector3(0.1, 0, 0.075) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/props/definitions/prop_tree_3.tres b/world/generation/props/definitions/prop_tree_3.tres index 58e3549..9f833c9 100644 --- a/world/generation/props/definitions/prop_tree_3.tres +++ b/world/generation/props/definitions/prop_tree_3.tres @@ -27,4 +27,5 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.55 collision_height = 4.2 +collision_offset = Vector3(-0.1, 0, 0.18) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/props/definitions/prop_tree_large.tres b/world/generation/props/definitions/prop_tree_large.tres index 9092ad1..ee91d62 100644 --- a/world/generation/props/definitions/prop_tree_large.tres +++ b/world/generation/props/definitions/prop_tree_large.tres @@ -25,6 +25,7 @@ variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) -collision_radius = 0.85 +collision_radius = 1.05 collision_height = 7.5 +collision_offset = Vector3(0.084, 0, 0.504) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/player_water_trigger.gd b/world/player_water_trigger.gd index 10fab14..ffe0582 100644 --- a/world/player_water_trigger.gd +++ b/world/player_water_trigger.gd @@ -29,9 +29,14 @@ enum EntryHeightReference { ) ## Recovery begins when the selected reference reaches this depth. @export_range(0.0, 2.0, 0.05) var entry_depth_threshold: float = 0.35 +## The player must remain below the threshold for this long before recovery. +## A short freshwater confirmation keeps jumps across stepping stones from +## being mistaken for a completed fall into the water. +@export_range(0.0, 1.0, 0.05) var entry_confirmation_seconds: float = 0.0 var _tracked_players: Array[Player] = [] var _triggered_players: Dictionary[StringName, bool] = {} +var _submerged_seconds: Dictionary[StringName, float] = {} func _ready() -> void: @@ -46,7 +51,7 @@ func _ready() -> void: set_physics_process(false) -func _physics_process(_delta: float) -> void: +func _physics_process(delta: float) -> void: var active_surface_height: float = get_surface_height() for player: Player in _tracked_players.duplicate(): if not is_instance_valid(player): @@ -58,9 +63,17 @@ func _physics_process(_delta: float) -> void: var entry_height := player.get_body_center_position().y if entry_height_reference == EntryHeightReference.PLAYER_ORIGIN: entry_height = player.global_position.y - if entry_height <= active_surface_height - entry_depth_threshold: - _triggered_players[player_key] = true - recovery_requested.emit(player, active_surface_height) + if entry_height > active_surface_height - entry_depth_threshold: + _submerged_seconds[player_key] = 0.0 + continue + var submerged_seconds: float = ( + _submerged_seconds.get(player_key, 0.0) + maxf(delta, 0.0) + ) + _submerged_seconds[player_key] = submerged_seconds + if submerged_seconds < entry_confirmation_seconds: + continue + _triggered_players[player_key] = true + recovery_requested.emit(player, active_surface_height) if _tracked_players.is_empty(): set_physics_process(false) @@ -86,6 +99,7 @@ func _on_body_entered(body: Node3D) -> void: if player == null or player in _tracked_players: return _tracked_players.append(player) + _submerged_seconds[StringName(str(player.get_instance_id()))] = 0.0 set_physics_process(true) @@ -94,6 +108,8 @@ func _on_body_exited(body: Node3D) -> void: if player == null: return _tracked_players.erase(player) - _triggered_players.erase(StringName(str(player.get_instance_id()))) + var player_key := StringName(str(player.get_instance_id())) + _triggered_players.erase(player_key) + _submerged_seconds.erase(player_key) if _tracked_players.is_empty(): set_physics_process(false) diff --git a/world/regions/starter_island_region.tscn b/world/regions/starter_island_region.tscn index b52b0ea..8f4bc4b 100644 --- a/world/regions/starter_island_region.tscn +++ b/world/regions/starter_island_region.tscn @@ -163,6 +163,7 @@ location_tags = Array[StringName]([&"starter_pond", &"pond"]) selection_priority = 1 recovery_entry_height_reference = 1 recovery_entry_depth_threshold = 0.1 +recovery_entry_confirmation_seconds = 0.2 [node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Pond" unique_id=1034265960] material_override = ExtResource("19_fresh_water") diff --git a/world/water_body_authoring.gd b/world/water_body_authoring.gd index a272d90..16636f7 100644 --- a/world/water_body_authoring.gd +++ b/world/water_body_authoring.gd @@ -80,6 +80,11 @@ var recovery_entry_depth_threshold: float = 0.35: set(value): recovery_entry_depth_threshold = clampf(value, 0.0, 2.0) _sync_owned_nodes() +@export_range(0.0, 1.0, 0.05) +var recovery_entry_confirmation_seconds: float = 0.0: + set(value): + recovery_entry_confirmation_seconds = clampf(value, 0.0, 1.0) + _sync_owned_nodes() @export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m") var recovery_depth: float = 4.8: set(value): @@ -173,6 +178,9 @@ func _sync_owned_nodes() -> void: recovery_region.entry_depth_threshold = ( recovery_entry_depth_threshold ) + recovery_region.entry_confirmation_seconds = ( + recovery_entry_confirmation_seconds + ) if uses_polygon and _sync_polygon_collision_shapes( recovery_region, recovery_shape_node,