extends SceneTree ## Fishing (rod, casting, bites, landing and losing fish, grilling) and the ## mixed diet (food groups, poor diet, malnutrition, balanced diet, saving). ## Run: godot --headless --path . --script res://tests/test_fishing_diet.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 _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 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("Fish", 1337) S.current_id = id var world: Node3D = await _spawn_world() 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 _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) ## Steps the float until it reaches `want` (or `secs` pass). Returns it. func _bob_until(world: Node3D, want: String, secs: float) -> Node3D: var t := 0.0 while t < secs: var b: Node3D = world.bobber if b == null or not is_instance_valid(b): return null if b.state == want: return b b.set_physics_process(false) b.step(0.05) t += 0.05 return world.bobber func _cast(world: Node3D, player: Node3D, toward: Vector3) -> bool: var origin: Vector3 = player.global_position + Vector3(0, 1.6, 0) return A.cast_line(player, origin, toward - origin, world) 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") player.set_physics_process(false) sv.set_process(false) world.day_night.frozen = true world.weather.force("clear") world.creatures.enabled = false world.day_night.time = 0.5 world.day_night.apply(true) print("the rod") var bench: Node = world.spawn_piece(DB.get_item("workbench"), Transform3D(Basis(), player.global_position + Vector3(2, -1.5, 0))) bench.global_position.y = terrain.sample_height(bench.global_position.x, bench.global_position.z) inv.add("stick", 2) inv.add("fiber", 3) inv.add("flint", 1) check(A.craft(player, "fishing_rod") and inv.count("fishing_rod") == 1, "a fishing rod from 2 sticks, 3 fiber and a flint") world.remove_piece(bench) _hold(player, "fishing_rod") check(A.melee_stats(player).armed == false, "(a rod isn't a weapon)") # Stand on the lake's east shore, looking out over the water. var lake: Vector2 = terrain.lake_center var x: float = lake.x while terrain.sample_height(x, lake.y) < terrain.water_level + 0.4 and x < lake.x + 60.0: x += 0.5 var shore := Vector3(x + 1.0, terrain.sample_height(x + 1.0, lake.y), lake.y) player.global_position = shore var out := Vector3(lake.x, terrain.water_level, lake.y) var inland := shore + Vector3(12, 0, 0) inland.y = terrain.sample_height(inland.x, inland.z) print("water and fish data") var wl: Dictionary = world.water_at(lake.x, lake.y) check(wl.get("type", "") == "lake" and wl.depth > 2.0, "the middle of the lake is deep lake water (%s)" % [wl]) check(world.water_at(shore.x + 3.0, shore.z).is_empty(), "the shore is dry land") var sp: Vector2 = terrain.coast_point(terrain.sea_angle_deg, 40.0) check(world.water_at(sp.x, sp.y).get("type", "") == "sea", "out past the cliffs is the sea") var lt: Dictionary = DB.water_fishing("lake") var ids := [] for f in lt.get("fish", []): ids.append(f.id) check(ids == ["perch", "trout"] and lt.min_depth == 0.5, "the lake's fish come from data/fish.json (%s)" % [ids]) check(DB.water_fishing("sea").is_empty(), "this island has no sea fish defined (the cliffs are in the way)") print("casting") sv.stamina = 100.0 check(_cast(world, player, out) and world.bobber != null, "cast the line") check(sv.stamina < 100.0, "casting costs a little stamina") var b := _bob_until(world, "floating", 5.0) check(b != null and b.state == "floating" and absf(b.global_position.y - terrain.water_level) < 0.01, "the float lands on the lake") check(terrain.sample_height(b.global_position.x, b.global_position.z) < terrain.water_level - 0.5, "over deep enough water (%.1f m)" % (terrain.water_level - terrain.sample_height(b.global_position.x, b.global_position.z))) b = _bob_until(world, "biting", 40.0) check(b != null and b.state == "biting" and _toasts.has("Bite! Click to reel in"), "a fish bites") var fish0 := inv.count("perch") + inv.count("trout") check(A.reel(player, world) and inv.count("perch") + inv.count("trout") == fish0 + 1, "reel in quickly and you land it") check(world.bobber == null, "and the line comes in") print("losing one") _cast(world, player, out) _bob_until(world, "floating", 5.0) b = _bob_until(world, "biting", 40.0) _toasts.clear() b.step(b.BITE_WINDOW + 0.1) check(b.state == "floating" and _toasts.has("It got away"), "too slow and it gets away") fish0 = inv.count("perch") + inv.count("trout") check(not A.reel(player, world) and inv.count("perch") + inv.count("trout") == fish0, "reeling in with nothing biting catches nothing") print("bad casts") _toasts.clear() player.global_position = shore _cast(world, player, inland + Vector3(10, 0, 0)) b = _bob_until(world, "landed", 5.0) check(b != null and b.state == "landed" and _toasts.any(func(t): return t.begins_with("Cast into the water")), "a cast onto dry land tells you so") b.step(60.0) check(b.state == "landed", "and nothing ever bites on land") A.reel(player, world) _cast(world, player, out) _bob_until(world, "floating", 5.0) player.global_position = shore + Vector3(45, 0, 0) world.bobber.step(0.05) check(world.bobber == null, "walk off and the line snaps") player.global_position = shore _cast(world, player, out) _bob_until(world, "floating", 5.0) inv.add("berries", 1) player.select(5) for _i in range(3): await physics_frame check(world.bobber == null, "switching away from the rod reels in") _hold(player, "fishing_rod") print("when they bite") _cast(world, player, out) b = _bob_until(world, "floating", 5.0) var noon := 0.0 var dawn := 0.0 world.day_night.time = 12.0 / 24.0 for _i in range(300): noon += b._next_wait() world.day_night.time = 6.5 / 24.0 for _i in range(300): dawn += b._next_wait() check(dawn < noon * 0.75, "fish bite sooner at dawn (%.1f s vs %.1f s at noon)" % [dawn / 300.0, noon / 300.0]) var trout := 0 for _i in range(400): if b._pick_fish() == "trout": trout += 1 check(trout > 100 and trout < 220, "trout are the rarer catch (%d in 400 at dawn)" % trout) A.reel(player, world) world.day_night.time = 0.5 print("grilling") inv.add("perch", 1) inv.add("trout", 1) var fire: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), shore + Vector3(2, 0, 0))) fire.global_position.y = terrain.sample_height(fire.global_position.x, fire.global_position.z) check(A.craft(player, "grilled_perch") and A.craft(player, "grilled_trout"), "grill perch and trout at a campfire") world.remove_piece(fire) print("diet") check(sv.diet.meat > 59.0 and sv.diet.fish > 59.0 and sv.diet.plants > 59.0, "a new world starts you reasonably fed (%s)" % [sv.diet]) sv.hunger = 50.0 inv.add("berries", 1) _hold(player, "berries") var p0: float = sv.diet.plants A.eat(player, 0) check(sv.diet.plants > p0 and sv.diet.meat < 60.0 + 0.01, "berries count as greens, not meat") _hold(player, "grilled_trout") sv.diet.fish = 20.0 var f0: float = sv.diet.fish A.eat(player, 0) check(sv.diet.fish >= f0 + 44.0, "grilled trout is fish") sv.diet = {"meat": 60.0, "fish": 60.0, "plants": 60.0} sv.hunger = 90.0 sv.warmth = 70.0 for _i in range(900): sv.step(1.0) check(sv.diet.meat < 15.0 + 0.5 and sv.diet.meat > 5.0, "each group runs down over about a day (meat %.0f after 15 min)" % sv.diet.meat) # One group low: no healing. sv.diet = {"meat": 50.0, "fish": 5.0, "plants": 50.0} sv.hunger = 90.0 sv.health = 60.0 sv._hurt_cd = 0.0 sv.step(10.0) check(sv.health == 60.0, "missing one food group stops natural healing") check(sv.statuses.any(func(s): return s.begins_with("Poor diet (need fish")), "and the HUD says what you're missing (%s)" % [sv.statuses]) # Two low: health drains. sv.diet = {"meat": 5.0, "fish": 5.0, "plants": 50.0} sv.step(10.0) check(sv.health < 60.0, "missing two makes you Malnourished: health drains (%.1f)" % sv.health) check(sv.statuses.any(func(s): return s.begins_with("Malnourished")), "(%s)" % [sv.statuses]) # Balanced heals faster than merely adequate. sv.diet = {"meat": 25.0, "fish": 25.0, "plants": 25.0} sv.health = 50.0 sv.step(10.0) var adequate: float = sv.health - 50.0 sv.diet = {"meat": 70.0, "fish": 70.0, "plants": 70.0} sv.health = 50.0 sv.step(10.0) var balanced: float = sv.health - 50.0 check(balanced > adequate * 1.4 and sv.statuses.has("Balanced diet"), "a balanced diet heals faster (%.1f vs %.1f per 10 s)" % [balanced, adequate]) # Living on berries alone eventually kills you. var deaths := [0] sv.died.connect(func(): deaths[0] += 1) sv.diet = {"meat": 60.0, "fish": 60.0, "plants": 60.0} sv.health = 100.0 sv.hunger = 100.0 inv.add("berries", 30) var hours := 0.0 var min_hunger := 100.0 while deaths[0] == 0 and hours < 72.0: for _i in range(60): sv.step(1.0) if deaths[0] == 0: min_hunger = minf(min_hunger, sv.hunger) if sv.hunger < 85.0: _hold(player, "berries") A.eat(player, 0) hours += 60.0 / 50.0 # 60 s is 1.2 in-game hours check(deaths[0] == 1 and min_hunger > 50.0, "berries alone keep you full but kill you in the end (after %.0f in-game hours, food never below %.0f)" % [hours, min_hunger]) check(hours > 24.0, "slowly, though: over a day, so there's time to notice") print("save + load") sv.diet = {"meat": 33.0, "fish": 44.0, "plants": 55.0} world.save_now() world.queue_free() await process_frame await process_frame world = await _spawn_world() sv = world.get_node("Player").survival check(absf(sv.diet.meat - 33.0) < 0.5 and absf(sv.diet.fish - 44.0) < 0.5 and absf(sv.diet.plants - 55.0) < 0.5, "diet levels are saved") sv.load_data({"health": 80.0}) check(sv.diet.meat == 60.0 and sv.diet.fish == 60.0, "older saves start with a reasonable diet") world.queue_free() _finished = true