extends SceneTree ## M1b: day/night, hunger, warmth, stamina, campfire fuel + heat + cooking, ## berry bushes, eating, death + grave, and saving all of it. ## ## Fast-forwards time by calling survival.step() / day_night.advance() / the ## fire's _process() directly, so the whole test runs in seconds. ## Run: godot --headless --path . --script res://tests/test_survival_stats.gd var _fails := 0 var _finished := false var A: Node var S: Node var DB: 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 _spawn_world() -> Node3D: var w: Node3D = load("res://scenes/world.tscn").instantiate() root.add_child(w) for _i in range(5): await physics_frame return w ## Advances the simulation `seconds` in 1 s steps (stats + clock + fires). func _ff(world: Node3D, seconds: float) -> void: var player: Node = world.get_node("Player") for _i in range(int(seconds)): world.day_night.advance(1.0) for f in world.get_tree().get_nodes_in_group("heat"): f._process(1.0) player.survival.step(1.0) func _run() -> void: A = root.get_node("Actions") S = root.get_node("Saves") DB = root.get_node("ItemDB") S.root_dir = "user://test_worlds" for w in S.list_worlds(): S.delete_world(w.id) var id: String = S.create_world("Stats", 1337) S.current_id = id var world: Node3D = await _spawn_world() await _body(world, id) 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 _body(world: Node3D, id: String) -> void: var player: CharacterBody3D = world.get_node("Player") var sv: Node = player.survival var dn: Node = world.day_night var inv: Inventory = player.inventory var terrain: Node3D = world.get_node("Terrain") player.set_physics_process(false) # keep the player put while we fast-forward sv.set_process(false) world.weather.force("clear") # weather has its own test (test_weather_building) print("day / night") check(dn.day == 1 and absf(dn.hours() - 8.0) < 0.1, "new world starts on day 1 at 08:00 (%s)" % dn.clock_text()) dn.time = 0.5; dn.apply(true) check(dn.daylight > 0.9, "noon is bright (daylight %.2f)" % dn.daylight) dn.time = 0.0; dn.apply(true) check(dn.daylight < 0.05, "midnight is dark (daylight %.2f)" % dn.daylight) dn.time = 0.99 dn.advance(dn.day_length * 0.02) check(dn.day == 2, "clock rolls over to day 2") print("hunger and eating") dn.time = 0.5; dn.apply(true) var h0: float = sv.hunger _ff(world, 300) check(sv.hunger < h0 - 15.0, "hunger drains over 5 min (%.0f -> %.0f)" % [h0, sv.hunger]) inv.add("berries", 3) _hold(player, "berries") var h1: float = sv.hunger check(A.eat(player, 0) and sv.hunger > h1 and inv.count("berries") == 2, "eating berries restores food") print("cold and fire") dn.time = 21.0 / 24.0; dn.apply(true) # night (fast-forward below stays before dawn) sv.warmth = 60.0 _ff(world, 240) check(sv.warmth < 30.0 and "Cold" in sv.statuses, "a night in the open gets cold (warmth %.0f, %s)" % [sv.warmth, sv.statuses]) var spot := player.global_position + Vector3(2.0, 0, 0) spot.y = terrain.sample_height(spot.x, spot.z) var fire: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), spot)) _ff(world, 30) check(sv.warmth > 55.0 and "Warm" in sv.statuses, "sitting by the fire warms you (warmth %.0f)" % sv.warmth) print("fuel and cooking") inv.add("mushroom", 2) check(A.can_craft(player, "roasted_mushroom"), "can roast a mushroom at a lit fire") fire._process(fire.fuel + 1.0) check(not fire.is_lit() and fire.heat_at(player.global_position) == 0.0, "the fire burns out when its fuel runs out") check(not A.can_craft(player, "roasted_mushroom"), "an unlit fire can't cook") inv.add("wood", 2) check(A.add_fuel(player, fire) and fire.is_lit() and inv.count("wood") == 1, "adding wood relights it") check(A.craft(player, "roasted_mushroom") and inv.count("roasted_mushroom") == 1, "roasted a mushroom") print("stamina") sv.stamina = 100.0 var swings := 0 while sv.use_stamina(6.0) and swings < 100: swings += 1 check(swings >= 14 and swings < 30, "about 16 swings empty your stamina (%d)" % swings) _ff(world, 6) check(sv.stamina > 90.0, "stamina regenerates after a pause") print("berry bushes") var bush: Node3D = world.get_node("Props").forage.values()[0] player.global_position = bush.global_position + Vector3(1.0, 0.3, 0) var b0 := inv.count("berries") check(A.forage(player, bush) and inv.count("berries") > b0 and inv.count("fiber") >= 1, "foraged berries and fiber") check(not A.forage(player, bush), "a picked bush is bare") bush._process(bush.regrow_seconds + 1.0) check(bush.ripe(), "it regrows") inv.add("stick", 1) inv.add("fiber", 2) check(A.craft(player, "torch") and inv.count("torch") == 1, "torch crafted from stick + fiber") _hold(player, "torch") check(player.hands.held_heat() > 0.0, "a held torch gives a little warmth") print("save + load") sv.hunger = 42.0 sv.warmth = 33.0 fire.fuel = 321.0 A.forage(player, bush) var regrow_left: float = bush.regrow_left dn.day = 3; dn.time = 0.75 world.save_now() world.queue_free() await process_frame await process_frame world = await _spawn_world() player = world.get_node("Player") sv = player.survival # (Warmth starts moving toward the air's again in the frames the world # takes to load, so it's only roughly where it was.) check(absf(sv.hunger - 42.0) < 0.5 and absf(sv.warmth - 33.0) < 3.0, "stats restored (food %.0f, warmth %.0f)" % [sv.hunger, sv.warmth]) check(world.day_night.day == 3 and absf(world.day_night.hours() - 18.0) < 0.2, "time of day restored (%s)" % world.day_night.clock_text()) var fire2: Node = null for p in world.get_node("Pieces").get_children(): if p.item_id == "campfire": fire2 = p check(fire2 != null and absf(fire2.fuel - 321.0) < 2.0 and fire2.is_lit(), "campfire fuel restored") var bush2: Node = world.get_node("Props").forage[bush.uid] if is_instance_valid(bush) else world.get_node("Props").forage.values()[0] check(absf(bush2.regrow_left - regrow_left) < 2.0, "bush regrow timer restored") print("death and grave") player.set_physics_process(false) sv.set_process(false) player.inventory.add("wood", 9) player.inventory.add("stone_axe", 1) var death_pos := player.global_position sv.hunger = 0.0 sv.health = 0.5 sv.step(2.0) check(sv.health == 50.0 and not sv.dead, "you respawn with half health") check(player.inventory.count("wood") == 0, "your inventory went into the grave") var grave: Node = null for p in world.get_node("Pieces").get_children(): if p.item_id == "grave": grave = p check(grave != null and grave.inventory.count("wood") >= 9 and grave.inventory.count("stone_axe") == 1, "the grave holds your things") check(grave != null and grave.global_position.distance_to(death_pos) < 15.0, "grave is where you fell") player.global_position = grave.global_position + Vector3(1, 0.5, 0) for i in range(grave.inventory.size()): if grave.inventory.get_slot(i) != null: A.transfer(player, grave.inventory, i, player.inventory, grave) await process_frame check(player.inventory.count("stone_axe") == 1, "recovered your belongings") check(not is_instance_valid(grave) or grave.is_queued_for_deletion(), "the empty grave crumbles") world.queue_free() _finished = true 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)