extends SceneTree ## P3 the bear (docs/PROGRESSION_DESIGN.md ยง4): its home in the forest by the ## rim, raiding unattended food (chests and food left out) but not food ## behind a palisade or in a closed hut, warning you off and charging, its ## drops (hide, fat, meat), the fur rug and coming back after it's killed. ## Run: godot --headless --path . --script res://tests/test_bear.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 bears: Node var bear: Node3D var bs: Node var real_home: 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("Bear", 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 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") bs = world.build bears = world.bears bear = bears.bear func _reload() -> void: check(world.save_now(), "saved") world.queue_free() await process_frame await _load() func _settle() -> void: for _i in range(3): await physics_frame 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) ## Runs the bear for up to `secs` (stopping early once `until` is true). func _sim(secs: float, until: Callable = Callable()) -> float: var t := 0.0 while t < secs: bears.step() if is_instance_valid(bear): bear.set_physics_process(false) bear.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 _toast_has(s: String) -> bool: for t in _toasts: if t.contains(s): return true return false func _ground(p: Vector3) -> Vector3: return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z) func _body() -> void: _home() await _day_and_night() await _raids() await _fenced() await _crowded() await _drops_and_rug() await _comes_back() _net() await _saving() _finished = true func _saving() -> void: print("saving") _set_time(8, 12.0) bear.hp = 150.0 await _reload() check(bears.alive() and absf(bear.hp - 150.0) < 0.01, "a hurt bear is still hurt after a reload (%.0f)" % bear.hp) bear.take_hit(500.0, bear.global_position + Vector3(2, 0, 0)) check(bears.dead_day == 8, "killed on day 8") await _reload() check(not bears.alive() and bears.dead_day == 8, "reloaded the same day: still no bear") _set_time(11, 7.0) player.global_position = world._spawn bears.step() check(bears.alive(), "and it's back three days on, at dawn") await _reload() check(bears.alive() and bears.dead_day == -1, "which is saved too") # --- home ---------------------------------------------------------------------------- func _home() -> void: print("the bear's home") check(bears != null and bears.found, "this island has a bear") bear = bears.bear check(bear != null and bear.kind == "bear" and bear.hp == 220.0, "one bear, 220 health") var h: Vector3 = bears.home real_home = h var trees := 0 for n in world.props.resource_nodes.values(): if n.kind == "tree" and (n as Node3D).global_position.distance_to(h) < 18.0: trees += 1 var rim: float = world.props._near_rim(h.x, h.z) check(trees >= 8 and rim > 0.1, "it lives in thick forest (%d trees within 18 m) near the rim (%.2f)" % [trees, rim]) var spawn: Vector3 = world._spawn check(Vector2(h.x - spawn.x, h.z - spawn.z).length() > 60.0 and h.distance_to(world.den.position) > 60.0, "away from the spawn (%.0f m) and the wolf den (%.0f m)" % [Vector2(h.x - spawn.x, h.z - spawn.z).length(), h.distance_to(world.den.position)]) var again: Node = load("res://scripts/creatures/bear_range.gd").new() again.world = world again._rng.seed = world.terrain.noise_seed + 881 check(again._find_home() and again.home.distance_to(h) < 0.01, "the same spot every time for this seed") again.free() func _day_and_night() -> void: print("its day") bears.enabled = false player.global_position = world._spawn # (well away, on the island) _set_time(2, 22.0) await _sim(30.0, func(): return bear.state == "rest") check(bear.state == "rest" and bear.global_position.distance_to(bears.home) < 3.0, "at night it sleeps at home") _set_time(2, 12.0) var far := 0.0 var t := 0.0 while t < 120.0: await _sim(5.0) far = maxf(far, bear.global_position.distance_to(bears.home)) t += 5.0 check(bear.state != "rest" and far > 3.0 and far < bears.HOME_RADIUS + 6.0, "by day it ambles round its patch (up to %.0f m out)" % far) check(not bears.raids_now(), "day 2: no raids yet") # --- raids ---------------------------------------------------------------------------- var site: Vector3 var yaw := 0.0 ## Moves the bear's patch (and the bear) next to the test site, a short ## walk from the food. func _bear_near(p: Vector3, d: float) -> void: bears.home = _ground(p + Vector3(-d, 0, 0)) bear.global_position = bears.home bear.state = "idle" bear._raid_cd = 0.0 bear._sniff = 0.0 bear._aggro = 0.0 bear.food = null func _chest(at: Vector3) -> Node3D: var c: Node3D = world.spawn_piece(DB.get_item("chest"), Transform3D(Basis(Vector3.UP, yaw), _ground(at))) c.inventory.add("berries", 5) c.inventory.add("raw_meat", 2) c.inventory.add("wood", 3) return c func _raids() -> void: print("raids") # Inland, away from the lake the spawn faces (dry, fairly flat ground). site = bs.debug_find_site(world._spawn + player.global_basis.z * 30.0, 6.0) yaw = player.rotation.y check(site.y > world.terrain.water_level + 0.5, "(test site on dry ground, %.1f m up)" % (site.y - world.terrain.water_level)) _set_time(4, 11.0) check(bears.raids_now(), "from day 3 it goes after food") var chest := _chest(site) await _settle() _bear_near(site, 22.0) player.global_position = _ground(site + Vector3(30, 0, 0)) _toasts.clear() var took: float = await _sim(60.0, func(): return chest.inventory.count("berries") == 0) check(chest.inventory.count("berries") == 0 and chest.inventory.count("raw_meat") == 0, "it smells an unattended chest 22 m off and eats the food (%.0f s)" % took) check(chest.inventory.count("wood") == 3, "but leaves the wood") check(_toast_has("bear"), "you hear about it (%s)" % [_toasts]) check(bear.state == "eat", "and sits there eating") await _sim(10.0) check(bear.state != "eat", "then wanders off") print("food left out") _bear_near(site, 18.0) var meat: Node3D = world.drop_item("raw_meat", 3, Transform3D(Basis(), _ground(site + Vector3(0, 0, 4)) + Vector3(0, 0.2, 0))) chest.queue_free() await _settle() await _sim(60.0, func(): return not is_instance_valid(meat) or meat.taken) check(not is_instance_valid(meat) or meat.taken, "meat left on the ground is eaten too") print("with you there") _bear_near(site, 20.0) var guarded := _chest(site + Vector3(0, 0, -3)) player.global_position = _ground(site + Vector3(6, 0, -3)) await _settle() await _sim(25.0) check(guarded.inventory.count("berries") == 5, "it leaves a chest alone while you're beside it") world.remove_piece(guarded) await _settle() func _fenced() -> void: print("a palisade") var c := site + Vector3(0, 0, 0) var pdef: Dictionary = DB.get_item("palisade") for i in [-1, 0, 1]: for side in [[Vector3(2 * i, 0, -3), 0.0], [Vector3(2 * i, 0, 3), 0.0], [Vector3(3, 0, 2 * i), PI * 0.5], [Vector3(-3, 0, 2 * i), PI * 0.5]]: var at: Vector3 = c + (side[0] as Vector3) world.spawn_piece(pdef, bs._free_xf("palisade", at, float(side[1]), true)) var inside := _chest(c) await _settle() check(bears.fenced(inside.global_position), "a chest inside a palisade ring is fenced in") _bear_near(site, 20.0) player.global_position = _ground(site + Vector3(40, 0, 0)) await _sim(45.0) check(inside.inventory.count("berries") == 5 and inside.inventory.count("raw_meat") == 2, "the bear can't get at it: the food is all there") check(bear.food == null, "and it isn't even trying") for p in world.all_pieces(): if p.global_position.distance_to(c) < 5.0: world.remove_piece(p) await _settle() print("a closed hut") var hut: Dictionary = await bs.debug_build_hut(site, 0.0) await _settle() var center: Vector3 = hut.pieces[0].global_transform * Vector3(1, 0, 1) var in_hut: Node3D = world.spawn_piece(DB.get_item("chest"), Transform3D(Basis(), center + Vector3(-0.8, 0, -0.8))) in_hut.inventory.add("cooked_meat", 4) await _settle() check(bears.fenced(in_hut.global_position) and not bears.raidable(in_hut), "food in a closed hut is safe too") var door: Node3D = null for p in hut.pieces: if p.item_id == "wood_door": door = p door.set_open(true, false) await _settle() check(bears.raidable(in_hut), "...unless you leave the door open") door.set_open(false, false) for p in world.all_pieces(): if p.global_position.distance_to(center) < 6.0: p.queue_free() await _settle() func _crowded() -> void: print("too close") _set_time(4, 12.0) _bear_near(site, 0.0) player.survival.health = 100.0 var to: Vector3 = bear.global_position player.global_position = _ground(to + Vector3(7.0, 0, 0)) await _sim(1.0) check(bear.state == "warn", "at 7 m it rears up and warns you off (%s)" % bear.state) check(bear.interact_prompt(player).contains("warning"), "\"%s\"" % bear.interact_prompt(player)) player.global_position = _ground(bear.global_position + Vector3(12.0, 0, 0)) await _sim(1.0) check(bear.state != "warn" and bear.state != "charge" and player.survival.health == 100.0, "back off and it leaves you be") player.global_position = _ground(bear.global_position + Vector3(4.5, 0, 0)) await _sim(8.0, func(): return player.survival.health < 100.0) check(player.survival.health < 100.0 and player.survival.health <= 100.0 - 20.0, "stay close and it charges: one blow takes %.0f health" % (100.0 - player.survival.health)) player.survival.health = 100.0 print("a fight") player.global_position = _ground(bear.global_position + Vector3(25.0, 0, 0)) await _sim(20.0, func(): return bear._aggro <= 0.0) bear._aggro = 0.0 bear.state = "idle" bear.take_hit(30.0, player.global_position) check(bear.state == "charge" and bear._aggro > 0.0, "shoot it and it comes for you") bear.take_hit(150.0, player.global_position) check(bear.state == "flee", "badly hurt (%.0f left), it runs" % bear.hp) func _drops_and_rug() -> void: print("its hide") bear.take_hit(500.0, player.global_position) check(bear.is_dead() and bears.dead_day == 4, "it can be killed") var got: Dictionary = bear.butcher_drops() check(int(got.get("bear_hide", 0)) == 1 and int(got.get("bear_fat", 0)) >= 2 and int(got.get("raw_meat", 0)) >= 5, "butchered: a hide, fat and plenty of meat (%s)" % [got]) check(int(DB.get_item("bear_fat").fuel) == 3 * int(DB.get_item("wood").fuel), "fat burns three times as long as wood") player.inventory.load_data(Inventory.new(40).to_data()) player.inventory.add("bear_fat", 1) var fire: Node3D = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), _ground(site + Vector3(10, 0, 10)))) fire.fuel = 10.0 player.global_position = fire.global_position + Vector3(1.2, 0, 0) check(A.add_fuel(player, fire) and absf(fire.fuel - 190.0) < 0.1, "and goes on a fire (%.0f s of fuel)" % fire.fuel) world.remove_piece(fire) print("a fur rug") var hut: Dictionary = await bs.debug_build_hut(site, 0.0) await _settle() var center: Vector3 = hut.pieces[0].global_transform * Vector3(1, 0, 1) player.inventory.add("bear_hide", 1) player.inventory.add("fiber", 2) var wb: Node3D = world.spawn_piece(DB.get_item("workbench"), Transform3D(Basis(), _ground(site + Vector3(8, 0, 0)))) player.global_position = wb.global_position + Vector3(0, 0, 1.4) check(A.craft(player, "fur_rug"), "a fur rug at the workbench (a bear hide, 2 fibre)") world.remove_piece(wb) var rdef: Dictionary = DB.get_item("fur_rug") var rxf := Transform3D(Basis(), center + Vector3(0.0, 0.0, 0.3)) check(world.placement_valid(rdef, rxf), "it goes on the hut floor") var rug: Node3D = world.spawn_piece(rdef, rxf) var hearth_fire: Node3D = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), center + Vector3(0.9, 0, -0.9))) var sv: Node = player.survival var spot := center + Vector3(-0.5, 0.05, 0.5) player.global_position = spot await _settle() sv._shelter_left = 0.0 sv.step(0.5) check(sv.cozy and "Cozy" in sv.statuses, "standing by it in the hut: Cozy (%s)" % [sv.statuses]) sv.rested = 0.0 sv._rest_build = 0.0 var secs := 0 while sv.rested <= 0.0 and secs < 40: sv.step(1.0) secs += 1 check(secs <= int(sv.REST_AFTER / 2.0) + 1, "Rested in %d s by the fire (half the usual %d)" % [secs, int(sv.REST_AFTER)]) var q := PhysicsRayQueryParameters3D.create(spot + Vector3(0, 1.0, 0), rug.global_position + Vector3(0, 0.02, 0), 1) check(world.get_world_3d().direct_space_state.intersect_ray(q).is_empty(), "and it's flat: you walk over it") rug.queue_free() hearth_fire.queue_free() await _settle() sv._shelter_left = 0.0 sv.step(0.5) check(not sv.cozy, "no rug, not cozy") for p in hut.pieces: if is_instance_valid(p): p.queue_free() await _settle() func _comes_back() -> void: print("another bear") bears.home = real_home # (the raids moved it next to the test site) bear = bears.bear _set_time(5, 7.0) bears.step() check(not bears.alive(), "the next morning: no bear") _set_time(7, 7.0) player.global_position = _ground(bears.home + Vector3(15, 0, 0)) bears.step() check(not bears.alive(), "three days on, not while you're standing at its home") player.global_position = world._spawn bears.step() check(bears.alive() and bears.bear != bear, "but once you've gone, there's a bear again") bear = bears.bear func _net() -> void: print("online") var CN: Node = root.get_node("CreatureNet") bear.set_meta("cid", 91) bear.state = "raid" var buf := PackedByteArray() buf.resize(CN.ENTRY) CN._encode(buf, 0, bear) var e: Array = CN._decode(buf, 0) check(e[1] == "bear" and e[4] == "raid", "the bear goes over the network (%s, %s)" % [e[1], e[4]])