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