extends SceneTree ## P3 thieves (docs/PROGRESSION_DESIGN.md ยง4): crows by day and foxes at ## night come for food left on the ground. A crow pecks a stack away and ## takes off if you come near; a fox runs off with the lot, and drops it if ## you kill it. Food you're standing by, and food in a chest, is safe. ## Run: godot --headless --path . --script res://tests/test_thieves.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 th: Node var site: Vector3 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("Thieves", 1337) 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 if world.bears.bear != null: world.bears.bear.queue_free() # (it would eat the test food) 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") th = world.thieves th.enabled = false 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 _set_time(hours: float) -> void: world.day_night.day = 3 world.day_night.time = hours / 24.0 world.day_night.apply(true) ## Runs the thieves for up to `secs`, stopping once `until` is true. func _sim(secs: float, until: Callable = Callable()) -> float: var t := 0.0 while t < secs: th.step(0.1) for c in th.get_children(): if is_instance_valid(c) and not c.is_queued_for_deletion(): c.set_physics_process(false) c.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 _ground(p: Vector3) -> Vector3: return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z) func _drop(id: String, n: int, at: Vector3) -> Node3D: return world.drop_item(id, n, Transform3D(Basis(), _ground(at) + Vector3(0, 0.15, 0))) func _toast_has(s: String) -> bool: for t in _toasts: if t.contains(s): return true return false func _thief(kind: String) -> Node3D: for c in th.alive(): if c.kind == kind: return c return null func _clear() -> void: for c in th.get_children(): c.queue_free() for d in world.all_dropped(): d.queue_free() await physics_frame func _body() -> void: site = world.build.debug_find_site(world._spawn + player.global_basis.z * 30.0, 6.0) await _crows() await _scared() await _safe() await _foxes() await _caught() _net() _finished = true func _crows() -> void: print("crows") _set_time(12.0) player.global_position = _ground(site + Vector3(25, 0, 0)) var berries := _drop("berries", 4, site) await _sim(15.0) check(th.alive().is_empty(), "food just put down draws nothing yet") await _sim(10.0, func(): return not th.alive().is_empty()) var crow := _thief("crow") check(crow != null, "left alone 20 s by day, it draws a crow") if crow == null: return check(crow.global_position.distance_to(berries.global_position) > 30.0 and crow.global_position.y > world.terrain.sample_height(crow.global_position.x, crow.global_position.z) + 4.0, "flying in from %.0f m away" % crow.global_position.distance_to(berries.global_position)) _toasts.clear() var t: float = await _sim(20.0, func(): return crow.state == "peck") check(crow.state == "peck" and crow.global_position.distance_to(berries.global_position) < 0.6, "it lands on the berries (%.0f s)" % t) check(_toast_has("crow is pecking"), "and you hear it (%s)" % [_toasts]) await _sim(6.5) check(int(berries.amount) == 2, "it eats them one every 3 s (%d left)" % int(berries.amount)) await _sim(8.0, func(): return berries.taken) check(berries.taken, "until they're gone") check(crow.state == "fly_off", "then it flies off") await _sim(25.0, func(): return not is_instance_valid(crow) or crow.is_queued_for_deletion()) check(not is_instance_valid(crow) or crow.is_queued_for_deletion(), "and is gone") await _clear() func _scared() -> void: print("scaring it off") var meat := _drop("cooked_meat", 3, site) await _sim(30.0, func(): return _thief("crow") != null and _thief("crow").state == "peck") var crow := _thief("crow") check(crow != null and crow.state == "peck", "a crow lands on some cooked meat") if crow == null: return player.global_position = _ground(site + Vector3(5, 0, 0)) await _sim(0.5) check(crow.state == "fly_off" and int(meat.amount) >= 2 and not meat.taken, "walk over and it takes off, leaving the meat (%d)" % int(meat.amount)) print("shooting it") crow.take_hit(10.0, player.global_position) check(crow.is_dead(), "an arrow brings it down") check(absf(crow.global_position.y - world.terrain.sample_height(crow.global_position.x, crow.global_position.z)) < 0.05, "it falls to the ground") var got: Dictionary = crow.butcher_drops() check(int(got.get("feather", 0)) >= 1, "feathers (%s)" % [got]) await _clear() func _safe() -> void: print("food you're watching") var by_you := _drop("berries", 5, site) player.global_position = _ground(site + Vector3(6, 0, 0)) await _sim(60.0) check(th.alive().is_empty() and int(by_you.amount) == 5, "a minute with you beside it: nothing comes") print("food in a chest") var chest: Node3D = world.spawn_piece(DB.get_item("chest"), Transform3D(Basis(), _ground(site + Vector3(0, 0, 4)))) chest.inventory.add("raw_meat", 4) by_you.queue_free() player.global_position = _ground(site + Vector3(25, 0, 0)) await _sim(60.0) check(th.alive().is_empty() and chest.inventory.count("raw_meat") == 4, "a closed chest keeps it safe") world.remove_piece(chest) await _clear() func _foxes() -> void: print("foxes") _set_time(21.0) var meat := _drop("raw_meat", 3, site) await _sim(25.0, func(): return not th.alive().is_empty()) var fox := _thief("fox") check(fox != null and _thief("crow") == null, "at night it's a fox that comes") if fox == null: return _toasts.clear() var t: float = await _sim(30.0, func(): return not fox.carry.is_empty()) check(meat.taken and int(fox.carry.get("count", 0)) == 3, "it slinks up and takes all 3 meat in one go (%.0f s)" % t) check(_toast_has("fox has run off"), "you hear it (%s)" % [_toasts]) await _sim(40.0, func(): return not is_instance_valid(fox) or fox.is_queued_for_deletion()) check(not is_instance_valid(fox) or fox.is_queued_for_deletion(), "it gets away: the meat's gone") var found: Array = world.all_dropped().filter(func(d): return d.item_id == "raw_meat") check(found.is_empty(), "(none left anywhere)") print("too close") var fish := _drop("trout", 1, site) await _sim(25.0, func(): return _thief("fox") != null) fox = _thief("fox") if fox != null: await _sim(4.0) player.global_position = fox.global_position + Vector3(6, 0, 0) await _sim(0.5) check(fox.state == "flee" and fox.carry.is_empty() and not fish.taken, "walk at it while it's creeping in and it bolts empty-handed") await _clear() func _caught() -> void: print("catching one") player.global_position = _ground(site + Vector3(25, 0, 0)) var meat := _drop("cooked_meat", 2, site) await _sim(25.0, func(): return _thief("fox") != null) var fox := _thief("fox") await _sim(30.0, func(): return fox != null and not fox.carry.is_empty()) check(fox != null and not fox.carry.is_empty(), "a fox runs off with your cooked meat") if fox == null: return await _sim(1.0) var at: Vector3 = fox.global_position fox.take_hit(40.0, player.global_position) check(fox.is_dead(), "you shoot it") var back: Array = world.all_dropped().filter(func(d): return d.item_id == "cooked_meat" and d.global_position.distance_to(at) < 1.5) check(back.size() == 1 and int(back[0].amount) == 2, "and it drops the meat where it fell") check(int(fox.butcher_drops().get("hide", 0)) == 1, "(and has a hide)") await _clear() func _net() -> void: print("online") var CN: Node = root.get_node("CreatureNet") for k in [["crow", "peck"], ["fox", "carry"]]: var c: Node3D = (th.CrowScript if k[0] == "crow" else th.FoxScript).new() c.setup(k[0], world, site, 5) c.thieves = th c.set_physics_process(false) th.add_child(c) c.set_meta("cid", 42) c.state = k[1] var buf := PackedByteArray() buf.resize(CN.ENTRY) CN._encode(buf, 0, c) var e: Array = CN._decode(buf, 0) check(e[1] == k[0] and e[4] == k[1], "a %s goes over the network (%s, %s)" % [k[0], e[1], e[4]]) c.free()