Wolf den search steps into the valley when the rim is steep everywhere

This commit is contained in:
Paul Bailey
2026-11-04 16:02:40 +00:00
parent d489bb57ed
commit 74cbf96458
3 changed files with 88 additions and 33 deletions

View File

@@ -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.

View File

@@ -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,

42
tests/test_den_sites.gd Normal file
View File

@@ -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)