From 74cbf96458ac19990e8308f3efa8367de1c8f5a2 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Wed, 4 Nov 2026 16:02:40 +0000 Subject: [PATCH] Wolf den search steps into the valley when the rim is steep everywhere --- scripts/creatures/den.gd | 77 +++++++++++++++++++++++----------------- tests/run_all.sh | 2 +- tests/test_den_sites.gd | 42 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 tests/test_den_sites.gd diff --git a/scripts/creatures/den.gd b/scripts/creatures/den.gd index 7dcfccf..48411e1 100644 --- a/scripts/creatures/den.gd +++ b/scripts/creatures/den.gd @@ -57,40 +57,53 @@ func _find_site(world: Node3D) -> Vector3: var space := world.get_world_3d().direct_space_state var box := BoxShape3D.new() box.size = Vector3(HALF_SIZE * 2.0, 6.0, HALF_SIZE * 2.0) - var ex: Array[RID] = [terrain.get_node("TerrainBody").get_rid(), world.player.get_rid()] - for step in range(52): - var ang := deg_to_rad(start + step * 7.0) - var dir := Vector2(cos(ang), sin(ang)) - # Walk inward from the rim until the den sits inside the walkable - # area, a little way short of the wall. - var r: float = terrain.play_radius + 10.0 - while r > terrain.play_radius - 40.0: - var x := dir.x * r - var z := dir.y * r - if terrain.inside_boundary(x, z, 11.0): - break - r -= 1.0 - var c := Vector3(dir.x * r, 0, dir.y * r) - if terrain.sea_weight(c.x, c.z) > 0.02 or Vector2(c.x, c.z).length() < 50.0: - continue - var hs: Array[float] = [] - for d in [Vector2(-1, -1), Vector2(1, -1), Vector2(-1, 1), Vector2(1, 1), Vector2.ZERO]: - hs.append(terrain.sample_height(c.x + d.x * 4.0, c.z + d.y * 4.0)) - if hs.max() - hs.min() > 2.2 or hs.min() < terrain.water_level + 1.0: - continue - c.y = hs[4] - # Trees and boulders in the way are cleared (_clear_area); just avoid - # the thickest woods. Counted by position, because their colliders - # were only just created and the physics space hasn't seen them yet. - var crowd := 0 - for uid in world.props.resource_nodes: - var n: Node3D = world.props.resource_nodes[uid] - if Vector2(n.global_position.x - c.x, n.global_position.z - c.z).length() < HALF_SIZE: - crowd += 1 - if crowd <= 5: - return c + # Pass 1 tries only the foot of the rim wall in each direction (the + # original search, so islands that already had a den keep it in the same + # place). Where the rim is steep all the way round (seed 777, seed 4), + # pass 2 steps further into the valley, a few metres at a time. + for pass_insets in [[0.0], [3.0, 6.0, 9.0, 12.0, 16.0, 20.0, 25.0, 30.0]]: + for step in range(52): + var ang := deg_to_rad(start + step * 7.0) + var dir := Vector2(cos(ang), sin(ang)) + var r_foot := _rim_foot(dir) + for inset in pass_insets: + var c := _check_site(world, dir * (r_foot - inset)) + if c != Vector3.INF: + return c return Vector3.INF +## Walks inward from the rim until the den sits inside the walkable area, a +## little way short of the wall. Returns that distance from the centre. +func _rim_foot(dir: Vector2) -> float: + var r: float = terrain.play_radius + 10.0 + while r > terrain.play_radius - 40.0: + if terrain.inside_boundary(dir.x * r, dir.y * r, 11.0): + break + r -= 1.0 + return r + +## The site at `p` (x, z) with its height, or INF if it's no good: near the +## sea or the spawn, too sloped, too low, or in thick woods. +func _check_site(world: Node3D, p: Vector2) -> Vector3: + if terrain.sea_weight(p.x, p.y) > 0.02 or p.length() < 50.0: + return Vector3.INF + var hs: Array[float] = [] + for d in [Vector2(-1, -1), Vector2(1, -1), Vector2(-1, 1), Vector2(1, 1), Vector2.ZERO]: + hs.append(terrain.sample_height(p.x + d.x * 4.0, p.y + d.y * 4.0)) + if hs.max() - hs.min() > 2.2 or hs.min() < terrain.water_level + 1.0: + return Vector3.INF + # Trees and boulders in the way are cleared (_clear_area); just avoid + # the thickest woods. Counted by position, because their colliders + # were only just created and the physics space hasn't seen them yet. + var crowd := 0 + for uid in world.props.resource_nodes: + var n: Node3D = world.props.resource_nodes[uid] + if Vector2(n.global_position.x - p.x, n.global_position.z - p.y).length() < HALF_SIZE: + crowd += 1 + if crowd > 5: + return Vector3.INF + return Vector3(p.x, hs[4], p.y) + ## Removes the trees, boulders, bushes and pickups the den is built over. ## Deterministic (the site comes from the seed), and the other scenery keeps ## its save ids because those come from creation order. diff --git a/tests/run_all.sh b/tests/run_all.sh index 5e160ea..229ac14 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then echo "Importing the project first (fresh copy)..." godot --headless --path . --import >/dev/null 2>&1 fi -for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_fishing_diet test_net_lobby test_net_world test_terrain_edges test_boundary; do +for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_den_sites test_fishing_diet test_net_lobby test_net_world test_terrain_edges test_boundary; do extra="" [ "$t" = "test_boundary" ] && extra="--fixed-fps 30" # A suite that waits on a world that failed to load would hang forever, diff --git a/tests/test_den_sites.gd b/tests/test_den_sites.gd new file mode 100644 index 0000000..786ddba --- /dev/null +++ b/tests/test_den_sites.gd @@ -0,0 +1,42 @@ +extends SceneTree +## Every island gets a wolf den: across many seeds the site search finds a +## spot away from the spawn and the sea. +## Run: godot --headless --path . --script res://tests/test_den_sites.gd + +const SEEDS := [777, 4, 1337, 1, 2, 3, 5, 6, 7, 8, 9, 10, 42, 99, 123, 2024, 4242, 9001, 31337, 65535] + +var _fails := 0 + +func check(cond: bool, what: String) -> void: + print((" ok " if cond else " FAIL ") + what) + if not cond: + _fails += 1 + +func _initialize() -> void: + _run.call_deferred() + +func _run() -> void: + var S: Node = root.get_node("Saves") + S.root_dir = "user://test_den_sites" + for seed in SEEDS: + for w in S.list_worlds(): + S.delete_world(w.id) + S.current_id = S.create_world("Den %d" % seed, seed) + var world: Node3D = load("res://scenes/world.tscn").instantiate() + root.add_child(world) + for _i in range(3): + await physics_frame + var den: Node3D = world.den + var t: Node3D = world.terrain + var p: Vector3 = den.position + var ok: bool = den.found + if ok: + ok = Vector2(p.x, p.z).length() >= 50.0 and t.sea_weight(p.x, p.z) <= 0.02 + check(ok, "seed %d has a den (%.0f m from the centre)" % [seed, Vector2(p.x, p.z).length()]) + world.queue_free() + await physics_frame + for w in S.list_worlds(): + S.delete_world(w.id) + DirAccess.remove_absolute(S.root_dir) + print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails]) + quit(1 if _fails > 0 else 0)