extends SceneTree ## P3 the barrow (docs/PROGRESSION_DESIGN.md ยง4): the burial mound in its ## stone ring, its passage and chamber, the guards that wake at night (or ## after day 10, or when the coffin's opened), clubs cracking bone, the loot ## and its key piece, restocking, and saving. ## Run: godot --headless --path . --script res://tests/test_barrow.gd var _fails := 0 var _finished := false var A: Node var S: Node var DB: Node var _toasts: Array[String] = [] var world: Node3D var player: CharacterBody3D var bw: 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: 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) S.current_id = S.create_world("Barrow", 1337) await _load() await _body() check(_finished, "test ran to the end (no script errors)") 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) func _load() -> void: world = load("res://scenes/world.tscn").instantiate() root.add_child(world) for _i in range(5): await physics_frame world.creatures.enabled = false for c in world.creatures.alive(): c.queue_free() if world.wolves != null: world.wolves.enabled = false world.bears.enabled = false world.thieves.enabled = false player = world.player player.set_physics_process(false) player.survival.set_process(false) world.day_night.frozen = true world.weather.set_process(false) world.weather.force("clear") bw = world.barrow bw.running = false # (the test steps it) for g in bw.guards: g.set_physics_process(false) func _reload() -> void: check(world.save_now(), "saved") world.queue_free() await process_frame await _load() func _set_time(day: int, hours: float) -> void: world.day_night.day = day world.day_night.time = hours / 24.0 world.day_night.apply(true) func _sim(secs: float, until: Callable = Callable()) -> float: var t := 0.0 while t < secs: bw.step() for g in bw.guards: if is_instance_valid(g): g.set_physics_process(false) g.step(0.1) t += 0.1 if until.is_valid() and until.call(): break if int(t * 10) % 10 == 0: await physics_frame return t func _awake() -> int: return bw.guards_alive().filter(func(g): return g.state == "charge").size() func _toast_has(s: String) -> bool: for t in _toasts: if t.contains(s): return true return false ## Stand somewhere in barrow-local space (feet on the floor or the ground). func _stand(lp: Vector3) -> void: var w: Vector3 = bw.to_world(lp) var f: float = bw.floor_y_at(w) w.y = f if f > -INF else world.terrain.sample_height(w.x, w.z) player.global_position = w + Vector3(0, 0.05, 0) player.velocity = Vector3.ZERO func _reset_guards() -> void: bw.alarmed = false for g in bw.guards_alive(): g.global_position = g.post g.state = "rest" g._aggro = 0.0 func _body() -> void: await _scenery() await _walk_in() await _by_day() await _loot() await _at_night() await _late() await _bone() await _restock() await _saving() _net() _finished = true # --- the mound ---------------------------------------------------------------------- func _scenery() -> void: print("the barrow") check(bw.found, "this island has a barrow") var c: Vector3 = bw.center() var spawn: Vector3 = world._spawn check(Vector2(c.x - spawn.x, c.z - spawn.z).length() > 55.0 and c.distance_to(world.den.position) > 70.0, "out in the valley, away from the spawn (%.0f m) and the den (%.0f m)" % [Vector2(c.x - spawn.x, c.z - spawn.z).length(), c.distance_to(world.den.position)]) check(Vector2(c.x - world.bears.home.x, c.z - world.bears.home.z).length() > 40.0, "and from the bear's home") var trees := 0 for n in world.props.resource_nodes.values(): if Vector2(n.global_position.x - c.x, n.global_position.z - c.z).length() < bw.CLEAR_R: trees += 1 check(trees == 0, "nothing grows inside the stone ring") var again: Node3D = load("res://scripts/world/barrow.gd").new() again.world = world again.terrain = world.terrain check(again._find_site().distance_to(c) < 0.01, "the same place every time for this seed") again.free() var stones := 0 for ch in bw.get_node("Ring").get_children(): stones += 1 check(stones >= 9, "ringed by %d standing stones" % stones) check(bw.guards.size() == 3 and bw.guards.all(func(g): return g.state == "rest"), "three guards stand at their posts") var inside: Vector3 = bw.to_world(Vector3(0, 0.05, 0.5)) check(world.build.roofed_at(inside), "the chamber has a roof (dry inside)") var top := PhysicsRayQueryParameters3D.create(c + Vector3(0, 10, 0), c + Vector3(0, -2, 0), 1) var hit := world.get_world_3d().direct_space_state.intersect_ray(top) check(not hit.is_empty() and hit.position.y > c.y + 2.8, "you can stand on top of the mound (%.1f m up)" % (hit.position.y - c.y if not hit.is_empty() else 0.0)) func _walk_in() -> void: print("going in") _set_time(3, 12.0) var m: Vector3 = bw.mouth() var along := PhysicsRayQueryParameters3D.create(bw.to_world(Vector3(0, 1.0, bw.PASSAGE_END + 1.0)), bw.to_world(Vector3(0, 1.0, 0.0)), 1) along.exclude = [player.get_rid()] check(world.get_world_3d().direct_space_state.intersect_ray(along).is_empty(), "the passage is open from the mouth to the chamber") player.global_position = m + Vector3(0, 0.1, 0) var to: Vector3 = bw.center() - m player.rotation = Vector3(0, atan2(-to.x, -to.z), 0) player.velocity = Vector3.ZERO player.set_physics_process(true) Input.action_press("move_forward") var got_in := false for _i in range(160): await physics_frame var l: Vector3 = bw.to_local_pos(player.global_position) if l.z < 0.6 and absf(l.x) < 2.0: got_in = true break Input.action_release("move_forward") for _i in range(5): await physics_frame player.set_physics_process(false) var l2: Vector3 = bw.to_local_pos(player.global_position) check(got_in, "you walk down the passage into the chamber (at %.1f, %.1f, %.1f local)" % [l2.x, l2.y, l2.z]) check(bw.inside(player.global_position), "(inside the barrow)") func _by_day() -> void: print("by day") _stand(Vector3(0, 0, 0.3)) await _sim(8.0) check(_awake() == 0, "day 3, noon, in the chamber: the guards don't stir") check(bw.guards[0].interact_prompt(player).contains("still"), "\"%s\"" % bw.guards[0].interact_prompt(player)) func _loot() -> void: print("the coffin") player.inventory.load_data(Inventory.new(40).to_data()) _stand(Vector3(0, 0, -0.5)) _toasts.clear() check(A.loot_barrow(player, bw), "you open the coffin") check(player.inventory.count("barrow_key") == 1, "a barrow key piece") check(player.inventory.count("iron_bar") >= 1 and player.inventory.count("iron_nugget") >= 3, "and iron (%d bars, %d nuggets)" % [player.inventory.count("iron_bar"), player.inventory.count("iron_nugget")]) check(_toast_has("something stirs"), "and something stirs (%s)" % [_toasts]) await _sim(0.5) check(_awake() == 3, "all three guards wake, even by day") check(not A.loot_barrow(player, bw) and _toast_has("empty") and player.inventory.count("barrow_key") == 1, "open it again: empty") print("getting away") var hp0: float = player.survival.health await _sim(6.0, func(): return player.survival.health < hp0) check(player.survival.health <= hp0 - 19.0, "stay and they hit hard (%.0f)" % (hp0 - player.survival.health)) player.survival.health = 100.0 var g: Node3D = bw.guards[0] check(float(g.def.run) < 5.0, "but they're slower than you walk (%.1f m/s)" % float(g.def.run)) _stand(Vector3(0, 0, 30)) await _sim(15.0) check(_awake() == 0 and bw.guards_alive().all(func(gg): return gg.state in ["home", "rest"]), "past the stone ring and the leash they give up") await _sim(25.0, func(): return bw.guards_alive().all(func(gg): return gg.state == "rest")) check(bw.guards_alive().all(func(gg): return gg.state == "rest" and gg.global_position.distance_to(gg.post) < 0.6), "and walk back to their posts") func _at_night() -> void: print("at night") _reset_guards() _set_time(4, 22.0) _stand(Vector3(0, 0, 14)) await _sim(3.0) check(_awake() == 0, "outside the ring at night, they stay put") _stand(Vector3(0, 0, 8)) await _sim(1.0) check(_awake() == 3, "step inside the ring at night and they come") player.inventory.set_slot(0, {"id": "torch", "count": 1}) player.select(0) await _sim(2.0) check(_awake() == 3, "a torch doesn't frighten them") _stand(Vector3(0, 0, 40)) await _sim(30.0, func(): return bw.guards_alive().all(func(gg): return gg.state == "rest")) func _late() -> void: print("after day 10") _reset_guards() _set_time(11, 12.0) _stand(Vector3(0, 0, 8)) await _sim(1.0) check(_awake() == 3, "day 11, noon: they wake by day too") _stand(Vector3(0, 0, 40)) await _sim(30.0, func(): return bw.guards_alive().all(func(gg): return gg.state == "rest")) _reset_guards() func _bone() -> void: print("bone") _set_time(3, 12.0) var g: Node3D = bw.guards[2] _stand(bw.to_local_pos(g.global_position) + Vector3(0, 0, 1.6)) player.inventory.set_slot(0, {"id": "club", "count": 1}) player.select(0) player.survival.stamina = 100.0 var hp0: float = g.hp check(A.melee(player, g), "a club...") var club: float = hp0 - g.hp player.inventory.set_slot(0, {"id": "iron_axe", "count": 1}) player.select(0) player.survival.stamina = 100.0 hp0 = g.hp A.melee(player, g) var axe: float = hp0 - g.hp check(absf(club - 18.0) < 0.01 and absf(axe - 14.0) < 0.01, "cracks bone: 18 a blow (12 +50%%), an iron axe 14 (%.0f, %.0f)" % [club, axe]) check(bw.alarmed and g.state == "charge", "hit one and they all wake") for gg in bw.guards_alive(): gg.take_hit(500.0, player.global_position) check(bw.guards_alive().is_empty() and bw.fallen.size() == 3, "all three fall") var drops: Dictionary = g.butcher_drops() check(int(drops.get("iron_nugget", 0)) >= 1, "their bones hold a little iron (%s)" % [drops]) func _restock() -> void: print("restocking") var looted: int = bw.loot_day check(looted == 3, "(emptied on day 3)") _set_time(looted + 4, 7.0) _stand(Vector3(0, 0, 60)) await _sim(0.3) check(not bw.stocked() and bw.guards_alive().is_empty(), "4 days on: still empty, no guards") _set_time(looted + 5, 7.0) _stand(Vector3(0, 0, 12)) await _sim(0.3) check(not bw.stocked(), "5 days on, but you're right there: not yet") _stand(Vector3(0, 0, 60)) await _sim(0.3) check(bw.stocked() and bw.guards_alive().size() == 3, "once you're away: the coffin's full and the guards are back") func _saving() -> void: print("saving") _set_time(9, 12.0) _stand(Vector3(0, 0, -0.5)) A.loot_barrow(player, bw) bw.guards[1].take_hit(500.0, player.global_position) await _reload() check(not bw.stocked() and bw.loot_day == 9, "the coffin is still empty after a reload") check(bw.guards.size() == 2 and bw.guards.all(func(g): return int(g.get_meta("post")) != 1), "and the fallen guard is still gone (%d standing)" % bw.guards.size()) check(player.inventory.count("barrow_key") == 2, "two key pieces now (%d)" % player.inventory.count("barrow_key")) func _net() -> void: print("online") var CN: Node = root.get_node("CreatureNet") var g: Node3D = bw.guards[0] g.set_meta("cid", 7) g.state = "charge" var buf := PackedByteArray() buf.resize(CN.ENTRY) CN._encode(buf, 0, g) var e: Array = CN._decode(buf, 0) check(e[1] == "guard" and e[4] == "charge", "a guard goes over the network (%s, %s)" % [e[1], e[4]])