Animals stay off the cliffs (Gitea #6): nothing walks up or along ground steeper than 35 degrees, and a carcass on steep ground slides down to where it can be reached and butchered
This commit is contained in:
@@ -92,6 +92,13 @@ const KINDS := {
|
||||
"drops": {"raw_meat": [5, 7], "bear_hide": [1, 1], "bear_fat": [2, 3]}},
|
||||
}
|
||||
const CARCASS_SECONDS := 300.0
|
||||
## The steepest ground an animal walks up: 35 degrees (the ground's normal
|
||||
## at least this upright). The player's limit is about 45.
|
||||
const MAX_CLIMB_NY := 0.82
|
||||
const MAX_CLIMB_RISE := 0.7 ## (tan 35 degrees: metres up per metre along)
|
||||
## A carcass on ground steeper than this slides down to where it's flatter,
|
||||
## so it can be reached and butchered (Gitea #6).
|
||||
const CARCASS_REST_NY := 0.8
|
||||
|
||||
var kind := "deer"
|
||||
var def: Dictionary
|
||||
@@ -384,6 +391,18 @@ func _blocked(dir: Vector3) -> bool:
|
||||
return true
|
||||
if absf(h - global_position.y) > 1.1:
|
||||
return true
|
||||
# Nothing climbs steep ground, or walks along it (the mountain rim: Gitea
|
||||
# #6). Going down it is fine, so one that's somehow up there can always
|
||||
# come back.
|
||||
if h > global_position.y - 0.3:
|
||||
var run := Vector2(probe.x - global_position.x, probe.z - global_position.z).length()
|
||||
if (h - global_position.y) > run * MAX_CLIMB_RISE:
|
||||
return true
|
||||
# The slope along the way: just ahead, halfway, and at the probe.
|
||||
for f in [0.2, 0.5, 1.0]:
|
||||
var s: Vector3 = global_position.lerp(probe, f)
|
||||
if terrain.sample_normal(s.x, s.z).y < MAX_CLIMB_NY and terrain.sample_height(s.x, s.z) > global_position.y - 0.3 * f:
|
||||
return true
|
||||
# Trees, rocks and buildings at chest height (the terrain isn't on this test).
|
||||
var from := global_position + Vector3(0, 0.45, 0)
|
||||
var q := PhysicsRayQueryParameters3D.create(from, Vector3(probe.x, h + 0.45, probe.z), 1)
|
||||
@@ -468,6 +487,7 @@ func _die(from: Vector3) -> void:
|
||||
if puppet:
|
||||
_visual.rotation.z = -PI * 0.5
|
||||
return
|
||||
_settle()
|
||||
# Topple onto its side, away from the blow.
|
||||
var side := 1.0 if (global_position - from).dot(global_basis.x) > 0.0 else -1.0
|
||||
if is_inside_tree() and DisplayServer.get_name() != "headless":
|
||||
@@ -477,6 +497,28 @@ func _die(from: Vector3) -> void:
|
||||
_visual.rotation.z = -side * PI * 0.5
|
||||
died.emit(self)
|
||||
|
||||
## A carcass on steep ground (half into a cliff face, out of reach) slides
|
||||
## downhill, half a metre at a time, until the ground's flatter: up to 20 m,
|
||||
## and never out past the edge of the play area. Online the server does this
|
||||
## and players see it where it ends up.
|
||||
func _settle() -> void:
|
||||
if terrain == null:
|
||||
return
|
||||
var p := global_position
|
||||
for _i in range(40):
|
||||
var n: Vector3 = terrain.sample_normal(p.x, p.z)
|
||||
if n.y >= CARCASS_REST_NY:
|
||||
break
|
||||
var down := Vector3(n.x, 0.0, n.z) # (a slope's normal leans downhill)
|
||||
if down.length() < 0.001:
|
||||
break
|
||||
var q := p + down.normalized() * 0.5
|
||||
if not terrain.inside_boundary(q.x, q.z, 1.0):
|
||||
break
|
||||
p = Vector3(q.x, terrain.sample_height(q.x, q.z), q.z)
|
||||
if p.distance_to(global_position) > 0.01:
|
||||
global_position = p
|
||||
|
||||
## What butchering gives (rolled once).
|
||||
func butcher_drops() -> Dictionary:
|
||||
var out := {}
|
||||
|
||||
@@ -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_boot test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_boat test_bug_report test_tunnel test_map test_terrain_edges test_boundary; do
|
||||
for t in test_boot test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_cliffs test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_boat test_bug_report test_tunnel test_map 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,
|
||||
|
||||
129
tests/test_cliffs.gd
Normal file
129
tests/test_cliffs.gd
Normal file
@@ -0,0 +1,129 @@
|
||||
extends SceneTree
|
||||
## Gitea #6: animals stay off the cliffs at the edge of the world. A deer
|
||||
## chased outward toward the mountain rim, all the way round, never climbs
|
||||
## steep ground; and one killed on a steep face comes to rest where it's
|
||||
## flatter, so you can reach it and butcher it.
|
||||
## Run: godot --headless --path . --script res://tests/test_cliffs.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var S: Node
|
||||
var A: Node
|
||||
|
||||
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:
|
||||
S = root.get_node("Saves")
|
||||
A = root.get_node("Actions")
|
||||
S.root_dir = "user://test_cliffs_worlds"
|
||||
for x in S.list_worlds():
|
||||
S.delete_world(x.id)
|
||||
S.current_id = S.create_world("Valley", 1337)
|
||||
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(w)
|
||||
for _i in range(8):
|
||||
await physics_frame
|
||||
w.creatures.enabled = false
|
||||
for c in w.creatures.get_children():
|
||||
c.queue_free()
|
||||
if w.wolves != null:
|
||||
w.wolves.enabled = false
|
||||
w.player.set_physics_process(false)
|
||||
w.player.survival.set_process(false)
|
||||
await _body(w)
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
for x in S.list_worlds():
|
||||
S.delete_world(x.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)
|
||||
|
||||
func _body(w: Node3D) -> void:
|
||||
var t: Node3D = w.terrain
|
||||
var p: Node3D = w.player
|
||||
print("chased toward the mountains")
|
||||
# How far into the mountain rim each deer gets (the terrain's own rim
|
||||
# distance), and the steepest ground it walks UP onto.
|
||||
# (Running down a steep bank is allowed: that's how one gets back down.)
|
||||
var steepest := 0.0
|
||||
var highest := 0.0
|
||||
var climb := 0.0
|
||||
var where := ""
|
||||
for i in range(36):
|
||||
var a := TAU * i / 36.0
|
||||
var dir := Vector3(cos(a), 0, sin(a))
|
||||
# (12 m inside the rim, which wobbles in and out by up to 16 m.)
|
||||
var start: Vector3 = dir * (t.play_radius - 12.0 - t._rim_offset(dir.x, dir.z))
|
||||
var d: Node3D = w.creatures.spawn("deer", start)
|
||||
d.set_physics_process(false)
|
||||
for _s in range(600): # (20 s at a run, with you right behind it)
|
||||
p.global_position = d.global_position - dir * 8.0
|
||||
d.player = p
|
||||
d._threat = p.global_position
|
||||
d.state = "flee"
|
||||
d._state_t = 4.0
|
||||
var y0: float = d.global_position.y
|
||||
d.step(1.0 / 30.0)
|
||||
var g: Vector3 = d.global_position
|
||||
if g.y > y0 + 0.0005:
|
||||
var deg := rad_to_deg(acos(clampf(t.sample_normal(g.x, g.z).y, -1.0, 1.0)))
|
||||
if deg > steepest:
|
||||
steepest = deg
|
||||
where = "%d degrees round" % (i * 10)
|
||||
highest = maxf(highest, t._rim_d(g.x, g.z) - t.play_radius)
|
||||
climb = maxf(climb, g.y - start.y - t.sample_height(start.x, start.z) + start.y)
|
||||
d.free()
|
||||
if i % 6 == 0:
|
||||
await process_frame
|
||||
# (Its limit is 35 degrees, checked at a few points ahead, so a few
|
||||
# degrees more can slip through between them; the player's limit is 45:
|
||||
# anywhere a deer goes, you can follow. Before the fix: 71 degrees.)
|
||||
check(steepest < 45.0, "a deer chased outward never walks up ground steeper than you can (steepest %.0f, at %s)" % [steepest, where])
|
||||
# (The rim rises 58 m over 30 m; its first few metres are still gentle.
|
||||
# Before the fix: 8 m in and up onto the cliff face.)
|
||||
check(highest < 6.0 and climb < 6.0, "and stays at the foot of the mountain rim (%.1f m in, %.1f m above where it started)" % [highest, climb])
|
||||
|
||||
print("killed on a cliff")
|
||||
# A steep spot on the rim, inside the play area: where a deer used to end up.
|
||||
var spot := Vector3.INF
|
||||
for i in range(72):
|
||||
var a := TAU * i / 72.0
|
||||
for r in range(int(t.play_radius) - 5, int(t.play_radius) + 15):
|
||||
var x := cos(a) * r
|
||||
var z := sin(a) * r
|
||||
if t.inside_boundary(x, z, 3.0) and t.sample_normal(x, z).y < 0.55:
|
||||
spot = Vector3(x, t.sample_height(x, z), z)
|
||||
break
|
||||
if spot != Vector3.INF:
|
||||
break
|
||||
check(spot != Vector3.INF, "(a 55-degree face on the rim to try it on)")
|
||||
if spot == Vector3.INF:
|
||||
return
|
||||
var deer: Node3D = w.creatures.spawn("deer", spot)
|
||||
deer.set_physics_process(false)
|
||||
deer.take_hit(999.0, spot + Vector3(0, 1, 0))
|
||||
var at: Vector3 = deer.global_position
|
||||
var ny: float = t.sample_normal(at.x, at.z).y
|
||||
check(deer.is_dead() and ny >= deer.CARCASS_REST_NY, "it comes to rest on flatter ground (%.0f degrees)" % rad_to_deg(acos(ny)))
|
||||
check(at.distance_to(spot) < 20.5 and at.y < spot.y, "a little way down the slope (%.1f m)" % at.distance_to(spot))
|
||||
check(absf(at.y - t.sample_height(at.x, at.z)) < 0.05, "lying on the ground, not inside it")
|
||||
p.global_position = at + Vector3(0.8, 0, 0)
|
||||
p.global_position.y = t.sample_height(p.global_position.x, p.global_position.z)
|
||||
var before: int = p.inventory.count("raw_meat")
|
||||
check(A.butcher(p, deer) and p.inventory.count("raw_meat") > before, "and you can butcher it")
|
||||
|
||||
print("on flat ground")
|
||||
var flat: Vector3 = w.build.debug_find_site(w._spawn, 3.0)
|
||||
var d2: Node3D = w.creatures.spawn("deer", flat)
|
||||
d2.set_physics_process(false)
|
||||
var was: Vector3 = d2.global_position
|
||||
d2.take_hit(999.0, was + Vector3(0, 1, 0))
|
||||
check(d2.global_position.distance_to(was) < 0.01, "a deer killed on the flat stays where it fell")
|
||||
d2.free()
|
||||
_finished = true
|
||||
1
tests/test_cliffs.gd.uid
Normal file
1
tests/test_cliffs.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b8pt3n5krip7j
|
||||
Reference in New Issue
Block a user