extends SceneTree ## Wolves: the den, the grace period, hunting at night, fire and walls as ## protection, pack tactics, timid lone wolves, the dawn retreat, den ## defence, pelts and the wolf cloak. ## Run: godot --headless --path . --script res://tests/test_wolves.gd var _fails := 0 var _finished := false var A: Node var S: Node var DB: Node var _toasts: Array[String] = [] 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: A = root.get_node("Actions") S = root.get_node("Saves") DB = root.get_node("ItemDB") A.notify.connect(func(t): _toasts.append(t)) S.root_dir = "user://test_worlds" for w in S.list_worlds(): S.delete_world(w.id) var id: String = S.create_world("Wolves", 1337) S.current_id = id var world: Node3D = load("res://scenes/world.tscn").instantiate() root.add_child(world) for _i in range(5): await physics_frame await _body(world) check(_finished, "test ran to the end (no script errors)") S.delete_world(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 _set_time(world: Node3D, day: int, hours: float) -> void: world.day_night.day = day world.day_night.time = hours / 24.0 world.day_night.apply(true) ## Runs the pack and every wolf for `secs`. Returns the most wolves that were ## darting in at once. func _sim(pack: Node, secs: float) -> int: var most := 0 var t := 0.0 while t < secs: pack.step(0.1) var lunging := 0 for w in pack.alive(): w.set_physics_process(false) w.step(0.1) if w.state == "lunge": lunging += 1 most = maxi(most, lunging) t += 0.1 return most func _nearest(pack: Node, p: Vector3) -> float: var best := INF for w in pack.alive(): best = minf(best, (w as Node3D).global_position.distance_to(p)) return best func _hold(player: Node, id: String) -> void: var inv: Inventory = player.inventory for i in range(inv.size()): var s = inv.get_slot(i) if s != null and s.id == id: var tmp = inv.get_slot(0) inv.set_slot(0, s) inv.set_slot(i, tmp) break player.select(0) func _toast_has(s: String) -> bool: for t in _toasts: if t.contains(s): return true return false func _body(world: Node3D) -> void: var player: CharacterBody3D = world.get_node("Player") var sv: Node = player.survival var inv: Inventory = player.inventory var terrain: Node3D = world.get_node("Terrain") var den: Node3D = world.den var pack: Node = world.wolves player.set_physics_process(false) sv.set_process(false) world.day_night.frozen = true world.weather.force("clear") world.creatures.enabled = false for c in world.creatures.get_children(): c.free() pack.enabled = false print("the den") check(den.found and pack != null, "this island has a wolf den") var dp: Vector3 = den.position check(terrain.sea_weight(dp.x, dp.z) < 0.05 and Vector2(dp.x, dp.z).length() > 50.0, "it's in the mountains, away from the sea and the spawn (%.0f m out)" % Vector2(dp.x, dp.z).length()) var mouth: Vector3 = den.mouth_pos() check(terrain.inside_boundary(mouth.x, mouth.z, 6.0), "its mouth opens onto walkable ground") check(Vector2(mouth.x, mouth.z).length() < Vector2(dp.x, dp.z).length(), "and faces into the valley") var trees := 0 for uid in world.props.resource_nodes: var n = world.props.resource_nodes[uid] if is_instance_valid(n) and not n.depleted and n.global_position.distance_to(dp) < 4.5: trees += 1 check(trees == 0, "no trees or boulders grow inside it") check(world.build.roofed_at(den.slot_pos(2)), "the cave has a rock roof") print("grace period") for d in [1, 2]: for h in [12.0, 22.0]: _set_time(world, d, h) _sim(pack, 0.5) check(pack.mode == "none" and pack.alive_count() == 0, "day %d %02d:00: no wolves yet" % [d, int(h)]) _set_time(world, 3, 12.0) _sim(pack, 3.0) check(pack.mode == "den" and pack.alive_count() == 2, "day 3: two wolves have moved into the den") var resting := 0 for w in pack.alive(): if w.state == "rest" and w.global_position.distance_to(den.slot_pos(w.slot)) < 1.5: resting += 1 check(resting == 2, "lying in the cave by day") # Somewhere open, well away from the den, to be hunted in. var site: Vector3 = world.build.debug_find_site(Vector3(0, 0, 0), 5.0) site.y = terrain.sample_height(site.x, site.z) var stand := site + Vector3(0, 0.02, 0) player.global_position = stand check(stand.distance_to(dp) > 45.0, "(test spot is %.0f m from the den)" % stand.distance_to(dp)) _set_time(world, 3, 21.0) _toasts.clear() _sim(pack, 20.0) check(_toast_has("howling"), "night 3: you hear them howl (%s)" % [_toasts]) check(pack.mode == "den" and _nearest(pack, dp) < 6.0 and sv.health == 100.0, "but they stay home") print("the hunt") _toasts.clear() _set_time(world, 4, 20.0) var most := _sim(pack, 45.0) check(_toast_has("hunting tonight"), "night 4: a warning that they're hunting") check(pack.mode == "hunt", "the pack is out") check(_nearest(pack, stand) < 18.0, "and has found you (nearest %.0f m)" % _nearest(pack, stand)) var waited := 0.0 while sv.health == 100.0 and waited < 60.0: most = maxi(most, _sim(pack, 1.0)) waited += 1.0 most = maxi(most, _sim(pack, 8.0)) check(sv.health < 100.0 and not sv.dead, "out in the open, they bite (health %.0f after %.0f s)" % [sv.health, waited]) check(most == 1, "one wolf darts in at a time (most at once: %d)" % most) sv.health = 100.0 player.global_position = stand print("fire") var fire: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), site + Vector3(1.5, 0, 0))) fire.fuel = fire.MAX_FUEL _sim(pack, 5.0) var closest := INF var t := 0.0 while t < 60.0: _sim(pack, 1.0) closest = minf(closest, _nearest(pack, stand)) t += 1.0 check(sv.health == 100.0, "beside a lit campfire they never close in") check(closest > 7.0, "they circle out beyond the firelight (closest %.0f m)" % closest) world.remove_piece(fire) print("torch") inv.add("torch", 1) _hold(player, "torch") pack.lunge_done(null) check(pack._gap >= pack.ATTACK_GAP * 2.0 * 0.8 - 0.01, "a torch makes them hesitate (%.1f s between darts)" % pack._gap) player.select(7) inv.set_slot(7, null) print("walls") var hut_site: Vector3 = world.build.debug_find_site(site + Vector3(14, 0, 0), 3.0) var hut: Dictionary = await world.build.debug_build_hut(hut_site, 0.0) check(hut.placed == 20, "(built a hut: %d pieces)" % hut.placed) var inside: Vector3 = hut.pieces[0].global_position + Vector3(1, 0.05, 1) player.global_position = inside sv.health = 100.0 _sim(pack, 60.0) check(sv.health == 100.0, "behind closed walls they can't reach you") var bed: Node = world.spawn_piece(DB.get_item("bed"), Transform3D(Basis(), inside + Vector3(0.0, -0.05, -0.2))) var w0: Node = pack.alive()[0] w0.state = "stalk" w0.global_position = inside + Vector3(6, 0, 0) check(world.sleep_block_reason(bed).contains("wolves"), "and you can't sleep with them prowling outside") world.remove_piece(bed) player.global_position = stand print("fighting back") inv.add("club", 1) _hold(player, "club") var a: Node = pack.alive()[0] var b: Node = pack.alive()[1] for w in pack.alive(): w._aggro = 0.0 a.global_position = stand + Vector3(1.2, 0, 0) sv.stamina = 100.0 A.melee(player, a) check(b._aggro > 0.0, "hit one and the whole pack turns on you") var hits := 1 while not a.wounded and not a.is_dead() and hits < 10: sv.stamina = 100.0 a.global_position = stand + Vector3(1.2, 0, 0) A.melee(player, a) hits += 1 check(a.wounded and a.state == "flee", "a badly hurt wolf runs (after %d blows, hp %.0f)" % [hits, a.hp]) while not a.is_dead() and hits < 20: sv.stamina = 100.0 a.global_position = stand + Vector3(1.2, 0, 0) A.melee(player, a) hits += 1 check(a.is_dead(), "but you can finish it") A.butcher(player, a) check(inv.count("wolf_pelt") == 1, "a wolf gives a pelt") print("a lone wolf") b._aggro = 0.0 b.wounded = false b.state = "stalk" b.global_position = stand + Vector3(7, 0, 0) b.global_position.y = terrain.sample_height(b.global_position.x, b.global_position.z) sv.health = 100.0 var lone_most := _sim(pack, 3.0) check(b.state == "flee" or b.global_position.distance_to(stand) > 9.0, "on its own it backs away (%s, %.0f m)" % [b.state, b.global_position.distance_to(stand)]) lone_most = maxi(lone_most, _sim(pack, 30.0)) check(lone_most == 0 and sv.health == 100.0, "and doesn't start a fight") print("dawn") player.global_position = stand _set_time(world, 5, 7.0) _sim(pack, 120.0) var home := 0 for w in pack.alive(): if w.global_position.distance_to(den.slot_pos(w.slot)) < 2.0: home += 1 check(pack.mode == "den" and home == pack.alive_count() and home > 0, "at dawn they go home (%d of %d)" % [home, pack.alive_count()]) check(pack.alive_count() == 2, "and the pack is back up to two") _set_time(world, 7, 12.0) _sim(pack, 1.0) check(pack.alive_count() == 3, "day 7: the pack has grown to three") check(pack.pack_size(10) == 4, "and it reaches four from day 9") print("the den by day") player.global_position = dp + (mouth - dp).normalized() * 12.0 player.global_position.y = terrain.sample_height(player.global_position.x, player.global_position.z) _sim(pack, 2.0) var watching := 0 for w in pack.alive(): if w.state == "alert": watching += 1 check(watching > 0 and sv.health == 100.0, "come near the den and they get up and watch you (%d)" % watching) player.global_position = mouth _sim(pack, 8.0) check(sv.health < 100.0, "come right up to it and they defend it (health %.0f)" % sv.health) sv.health = 100.0 print("wolf cloak") var bench: Node = world.spawn_piece(DB.get_item("workbench"), Transform3D(Basis(), stand + Vector3(-2.5, -0.02, 0))) player.global_position = stand inv.add("wolf_pelt", 2) inv.add("hide", 2) inv.add("fiber", 4) check(A.craft(player, "wolf_cloak") and inv.count("wolf_cloak") == 1, "a wolf cloak from 3 pelts, 2 hide and 4 fiber") world.remove_piece(bench) check(float(DB.get_item("wolf_cloak").wear.warmth) > float(DB.get_item("hide_cloak").wear.warmth), "warmer than the hide cloak") world.queue_free() _finished = true