Wolves with a den, fishing, and a mixed diet
Wolves - Den: a boulder cave at the foot of the rim, sited from the seed (away from the sea and spawn, mouth toward the valley, clearing in front) - Grace period: no wolves days 1-2; they move in on day 3 and howl; hunting from night 4; pack of 2, 3 from day 6, 4 from day 9; replaced by day - Wary pack hunters: circle and dart in one at a time; a lit campfire is a safe zone, a torch makes them hesitate, walls block bites (line of sight); hit one and the pack turns; hurt wolves flee; a lone wolf is timid; by day they defend the den; home at dawn - Glowing eyes, synthesised howls, can't sleep with wolves prowling - Wolf pelt and Wolf Cloak (+25 warmth) - Animals take a committed detour around obstacles Fishing - Fishing rod (workbench); cast a float onto the lake, 1.5 s to land a bite - Perch and rarer trout; faster bites at dawn/dusk and in rain; grill them - Visible fish near the shore that jump with splash rings Mixed diet - Foods belong to meat, fish or greens; each level runs down over ~1.5 days - One group low: no healing; two low: Malnourished (health drains); all three good: Balanced diet (heal x1.5); HUD diet bars; saved Tests: test_wolves and test_fishing_diet; run_all.sh covers 9 suites
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
# Usage: tests/run_all.sh (from the repo root; needs `godot` on PATH)
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
status=0
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_terrain_edges test_boundary; do
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_fishing_diet test_terrain_edges test_boundary; do
|
||||
extra=""
|
||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||
# A suite that waits on a world that failed to load would hang forever,
|
||||
|
||||
262
tests/test_fishing_diet.gd
Normal file
262
tests/test_fishing_diet.gd
Normal file
@@ -0,0 +1,262 @@
|
||||
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("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
|
||||
280
tests/test_wolves.gd
Normal file
280
tests/test_wolves.gd
Normal file
@@ -0,0 +1,280 @@
|
||||
extends SceneTree
|
||||
## Wolves: the den, the grace period, hunting at night, fire and walls as
|
||||
## protection, pack tactics, timid lone wolves, the dawn retreat, den
|
||||
## defence, pelts and the wolf cloak.
|
||||
## Run: godot --headless --path . --script res://tests/test_wolves.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 _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("Wolves", 1337)
|
||||
S.current_id = id
|
||||
var world: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
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 _set_time(world: Node3D, day: int, hours: float) -> void:
|
||||
world.day_night.day = day
|
||||
world.day_night.time = hours / 24.0
|
||||
world.day_night.apply(true)
|
||||
|
||||
## Runs the pack and every wolf for `secs`. Returns the most wolves that were
|
||||
## darting in at once.
|
||||
func _sim(pack: Node, secs: float) -> int:
|
||||
var most := 0
|
||||
var t := 0.0
|
||||
while t < secs:
|
||||
pack.step(0.1)
|
||||
var lunging := 0
|
||||
for w in pack.alive():
|
||||
w.set_physics_process(false)
|
||||
w.step(0.1)
|
||||
if w.state == "lunge":
|
||||
lunging += 1
|
||||
most = maxi(most, lunging)
|
||||
t += 0.1
|
||||
return most
|
||||
|
||||
func _nearest(pack: Node, p: Vector3) -> float:
|
||||
var best := INF
|
||||
for w in pack.alive():
|
||||
best = minf(best, (w as Node3D).global_position.distance_to(p))
|
||||
return best
|
||||
|
||||
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)
|
||||
|
||||
func _toast_has(s: String) -> bool:
|
||||
for t in _toasts:
|
||||
if t.contains(s):
|
||||
return true
|
||||
return false
|
||||
|
||||
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")
|
||||
var den: Node3D = world.den
|
||||
var pack: Node = world.wolves
|
||||
player.set_physics_process(false)
|
||||
sv.set_process(false)
|
||||
world.day_night.frozen = true
|
||||
world.weather.force("clear")
|
||||
world.creatures.enabled = false
|
||||
for c in world.creatures.get_children():
|
||||
c.free()
|
||||
pack.enabled = false
|
||||
|
||||
print("the den")
|
||||
check(den.found and pack != null, "this island has a wolf den")
|
||||
var dp: Vector3 = den.position
|
||||
check(terrain.sea_weight(dp.x, dp.z) < 0.05 and Vector2(dp.x, dp.z).length() > 50.0, "it's in the mountains, away from the sea and the spawn (%.0f m out)" % Vector2(dp.x, dp.z).length())
|
||||
var mouth: Vector3 = den.mouth_pos()
|
||||
check(terrain.inside_boundary(mouth.x, mouth.z, 6.0), "its mouth opens onto walkable ground")
|
||||
check(Vector2(mouth.x, mouth.z).length() < Vector2(dp.x, dp.z).length(), "and faces into the valley")
|
||||
var trees := 0
|
||||
for uid in world.props.resource_nodes:
|
||||
var n = world.props.resource_nodes[uid]
|
||||
if is_instance_valid(n) and not n.depleted and n.global_position.distance_to(dp) < 4.5:
|
||||
trees += 1
|
||||
check(trees == 0, "no trees or boulders grow inside it")
|
||||
check(world.build.roofed_at(den.slot_pos(2)), "the cave has a rock roof")
|
||||
|
||||
print("grace period")
|
||||
for d in [1, 2]:
|
||||
for h in [12.0, 22.0]:
|
||||
_set_time(world, d, h)
|
||||
_sim(pack, 0.5)
|
||||
check(pack.mode == "none" and pack.alive_count() == 0, "day %d %02d:00: no wolves yet" % [d, int(h)])
|
||||
_set_time(world, 3, 12.0)
|
||||
_sim(pack, 3.0)
|
||||
check(pack.mode == "den" and pack.alive_count() == 2, "day 3: two wolves have moved into the den")
|
||||
var resting := 0
|
||||
for w in pack.alive():
|
||||
if w.state == "rest" and w.global_position.distance_to(den.slot_pos(w.slot)) < 1.5:
|
||||
resting += 1
|
||||
check(resting == 2, "lying in the cave by day")
|
||||
|
||||
# Somewhere open, well away from the den, to be hunted in.
|
||||
var site: Vector3 = world.build.debug_find_site(Vector3(0, 0, 0), 5.0)
|
||||
site.y = terrain.sample_height(site.x, site.z)
|
||||
var stand := site + Vector3(0, 0.02, 0)
|
||||
player.global_position = stand
|
||||
check(stand.distance_to(dp) > 45.0, "(test spot is %.0f m from the den)" % stand.distance_to(dp))
|
||||
_set_time(world, 3, 21.0)
|
||||
_toasts.clear()
|
||||
_sim(pack, 20.0)
|
||||
check(_toast_has("howling"), "night 3: you hear them howl (%s)" % [_toasts])
|
||||
check(pack.mode == "den" and _nearest(pack, dp) < 6.0 and sv.health == 100.0, "but they stay home")
|
||||
|
||||
print("the hunt")
|
||||
_toasts.clear()
|
||||
_set_time(world, 4, 20.0)
|
||||
var most := _sim(pack, 45.0)
|
||||
check(_toast_has("hunting tonight"), "night 4: a warning that they're hunting")
|
||||
check(pack.mode == "hunt", "the pack is out")
|
||||
check(_nearest(pack, stand) < 18.0, "and has found you (nearest %.0f m)" % _nearest(pack, stand))
|
||||
var waited := 0.0
|
||||
while sv.health == 100.0 and waited < 60.0:
|
||||
most = maxi(most, _sim(pack, 1.0))
|
||||
waited += 1.0
|
||||
most = maxi(most, _sim(pack, 8.0))
|
||||
check(sv.health < 100.0 and not sv.dead, "out in the open, they bite (health %.0f after %.0f s)" % [sv.health, waited])
|
||||
check(most == 1, "one wolf darts in at a time (most at once: %d)" % most)
|
||||
sv.health = 100.0
|
||||
player.global_position = stand
|
||||
|
||||
print("fire")
|
||||
var fire: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), site + Vector3(1.5, 0, 0)))
|
||||
fire.fuel = fire.MAX_FUEL
|
||||
_sim(pack, 5.0)
|
||||
var closest := INF
|
||||
var t := 0.0
|
||||
while t < 60.0:
|
||||
_sim(pack, 1.0)
|
||||
closest = minf(closest, _nearest(pack, stand))
|
||||
t += 1.0
|
||||
check(sv.health == 100.0, "beside a lit campfire they never close in")
|
||||
check(closest > 7.0, "they circle out beyond the firelight (closest %.0f m)" % closest)
|
||||
world.remove_piece(fire)
|
||||
|
||||
print("torch")
|
||||
inv.add("torch", 1)
|
||||
_hold(player, "torch")
|
||||
pack.lunge_done(null)
|
||||
check(pack._gap >= pack.ATTACK_GAP * 2.0 * 0.8 - 0.01, "a torch makes them hesitate (%.1f s between darts)" % pack._gap)
|
||||
player.select(7)
|
||||
inv.set_slot(7, null)
|
||||
|
||||
print("walls")
|
||||
var hut_site: Vector3 = world.build.debug_find_site(site + Vector3(14, 0, 0), 3.0)
|
||||
var hut: Dictionary = await world.build.debug_build_hut(hut_site, 0.0)
|
||||
check(hut.placed == 20, "(built a hut: %d pieces)" % hut.placed)
|
||||
var inside: Vector3 = hut.pieces[0].global_position + Vector3(1, 0.05, 1)
|
||||
player.global_position = inside
|
||||
sv.health = 100.0
|
||||
_sim(pack, 60.0)
|
||||
check(sv.health == 100.0, "behind closed walls they can't reach you")
|
||||
var bed: Node = world.spawn_piece(DB.get_item("bed"), Transform3D(Basis(), inside + Vector3(0.0, -0.05, -0.2)))
|
||||
var w0: Node = pack.alive()[0]
|
||||
w0.state = "stalk"
|
||||
w0.global_position = inside + Vector3(6, 0, 0)
|
||||
check(world.sleep_block_reason(bed).contains("wolves"), "and you can't sleep with them prowling outside")
|
||||
world.remove_piece(bed)
|
||||
player.global_position = stand
|
||||
|
||||
print("fighting back")
|
||||
inv.add("club", 1)
|
||||
_hold(player, "club")
|
||||
var a: Node = pack.alive()[0]
|
||||
var b: Node = pack.alive()[1]
|
||||
for w in pack.alive():
|
||||
w._aggro = 0.0
|
||||
a.global_position = stand + Vector3(1.2, 0, 0)
|
||||
sv.stamina = 100.0
|
||||
A.melee(player, a)
|
||||
check(b._aggro > 0.0, "hit one and the whole pack turns on you")
|
||||
var hits := 1
|
||||
while not a.wounded and not a.is_dead() and hits < 10:
|
||||
sv.stamina = 100.0
|
||||
a.global_position = stand + Vector3(1.2, 0, 0)
|
||||
A.melee(player, a)
|
||||
hits += 1
|
||||
check(a.wounded and a.state == "flee", "a badly hurt wolf runs (after %d blows, hp %.0f)" % [hits, a.hp])
|
||||
while not a.is_dead() and hits < 20:
|
||||
sv.stamina = 100.0
|
||||
a.global_position = stand + Vector3(1.2, 0, 0)
|
||||
A.melee(player, a)
|
||||
hits += 1
|
||||
check(a.is_dead(), "but you can finish it")
|
||||
A.butcher(player, a)
|
||||
check(inv.count("wolf_pelt") == 1, "a wolf gives a pelt")
|
||||
|
||||
print("a lone wolf")
|
||||
b._aggro = 0.0
|
||||
b.wounded = false
|
||||
b.state = "stalk"
|
||||
b.global_position = stand + Vector3(7, 0, 0)
|
||||
b.global_position.y = terrain.sample_height(b.global_position.x, b.global_position.z)
|
||||
sv.health = 100.0
|
||||
var lone_most := _sim(pack, 3.0)
|
||||
check(b.state == "flee" or b.global_position.distance_to(stand) > 9.0, "on its own it backs away (%s, %.0f m)" % [b.state, b.global_position.distance_to(stand)])
|
||||
lone_most = maxi(lone_most, _sim(pack, 30.0))
|
||||
check(lone_most == 0 and sv.health == 100.0, "and doesn't start a fight")
|
||||
|
||||
print("dawn")
|
||||
player.global_position = stand
|
||||
_set_time(world, 5, 7.0)
|
||||
_sim(pack, 120.0)
|
||||
var home := 0
|
||||
for w in pack.alive():
|
||||
if w.global_position.distance_to(den.slot_pos(w.slot)) < 2.0:
|
||||
home += 1
|
||||
check(pack.mode == "den" and home == pack.alive_count() and home > 0, "at dawn they go home (%d of %d)" % [home, pack.alive_count()])
|
||||
check(pack.alive_count() == 2, "and the pack is back up to two")
|
||||
_set_time(world, 7, 12.0)
|
||||
_sim(pack, 1.0)
|
||||
check(pack.alive_count() == 3, "day 7: the pack has grown to three")
|
||||
check(pack.pack_size(10) == 4, "and it reaches four from day 9")
|
||||
|
||||
print("the den by day")
|
||||
player.global_position = dp + (mouth - dp).normalized() * 12.0
|
||||
player.global_position.y = terrain.sample_height(player.global_position.x, player.global_position.z)
|
||||
_sim(pack, 2.0)
|
||||
var watching := 0
|
||||
for w in pack.alive():
|
||||
if w.state == "alert":
|
||||
watching += 1
|
||||
check(watching > 0 and sv.health == 100.0, "come near the den and they get up and watch you (%d)" % watching)
|
||||
player.global_position = mouth
|
||||
_sim(pack, 8.0)
|
||||
check(sv.health < 100.0, "come right up to it and they defend it (health %.0f)" % sv.health)
|
||||
sv.health = 100.0
|
||||
|
||||
print("wolf cloak")
|
||||
var bench: Node = world.spawn_piece(DB.get_item("workbench"), Transform3D(Basis(), stand + Vector3(-2.5, -0.02, 0)))
|
||||
player.global_position = stand
|
||||
inv.add("wolf_pelt", 2)
|
||||
inv.add("hide", 2)
|
||||
inv.add("fiber", 4)
|
||||
check(A.craft(player, "wolf_cloak") and inv.count("wolf_cloak") == 1, "a wolf cloak from 3 pelts, 2 hide and 4 fiber")
|
||||
world.remove_piece(bench)
|
||||
check(float(DB.get_item("wolf_cloak").wear.warmth) > float(DB.get_item("hide_cloak").wear.warmth), "warmer than the hide cloak")
|
||||
world.queue_free()
|
||||
_finished = true
|
||||
Reference in New Issue
Block a user