Files
Emberwild/tests/test_creatures_combat.gd

381 lines
15 KiB
GDScript

extends SceneTree
## M1d: wildlife (spawning, senses, fleeing, boars charging), melee, thrown
## spears, the bow, butchering, cooking and food buffs, the hide cloak,
## dropped items, and saving all of it.
## Run: godot --headless --path . --script res://tests/test_creatures_combat.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
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("Hunt", 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)
## Runs a creature's logic for `secs` in small steps.
func _sim(c: Node, secs: float) -> void:
var t := 0.0
while t < secs and is_instance_valid(c):
c.step(0.05)
t += 0.05
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 _clear(sp: Node) -> void:
for c in sp.get_children():
c.free()
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 sp: Node = world.creatures
var cam: Camera3D = player.get_node("Camera3D")
player.set_physics_process(false)
sv.set_process(false)
world.weather.force("clear")
world.day_night.time = 0.5
world.day_night.apply(true)
print("spawning")
var counts := {}
var bad := 0
var nearest := INF
for c in sp.alive():
counts[c.kind] = counts.get(c.kind, 0) + 1
var p: Vector3 = c.global_position
if not terrain.inside_boundary(p.x, p.z, 0.0) or p.y < terrain.water_level:
bad += 1
nearest = minf(nearest, p.distance_to(player.global_position))
check(counts.get("rabbit", 0) >= 4 and counts.get("deer", 0) >= 2 and counts.get("boar", 0) >= 1,
"a new world has wildlife (%s)" % counts)
check(bad == 0, "every animal spawned on dry land inside the island")
check(nearest > 20.0, "none spawned on top of you (nearest %.0f m)" % nearest)
var feathers := 0
for uid in world.props.pickups:
if world.props.pickups[uid].item_id == "feather":
feathers += 1
check(feathers >= 50, "feathers lie in the meadows (%d)" % feathers)
sp.enabled = false
_clear(sp)
# A clear, flat spot to hunt on, with the player facing along +X there.
var site: Vector3 = world.build.debug_find_site(player.global_position + Vector3(10, 0, 0), 6.0)
site.y = terrain.sample_height(site.x, site.z)
player.global_position = site + Vector3(0, 0.02, 0)
player.rotation = Vector3(0, -PI * 0.5, 0) # forward (-Z) turned to +X
cam.rotation.x = 0.0
var fwd := -player.global_basis.z
player.velocity = Vector3.ZERO
print("senses")
var rabbit: Node = sp.spawn("rabbit", site + fwd * 4.0)
var r0: float = rabbit.global_position.distance_to(player.global_position)
_sim(rabbit, 2.0)
check(rabbit.state == "flee" and rabbit.global_position.distance_to(player.global_position) > r0 + 4.0,
"a rabbit 4 m away bolts (%s, %.0f m -> %.0f m)" % [rabbit.state, r0, rabbit.global_position.distance_to(player.global_position)])
rabbit.free()
var deer: Node = sp.spawn("deer", site + fwd * 22.0)
_sim(deer, 1.0)
check(deer.state != "flee", "a deer 22 m away doesn't notice you standing still")
player.velocity = fwd * 8.0
_sim(deer, 1.0)
check(deer.state == "flee", "but hears you sprinting")
player.velocity = Vector3.ZERO
deer.free()
var boar: Node = sp.spawn("boar", site + fwd * 3.0)
var h0: float = sv.health
_sim(boar, 3.0)
check(boar.state != "charge" and sv.health == h0, "a boar leaves you alone if you leave it alone")
boar.free()
print("melee")
rabbit = sp.spawn("rabbit", site + fwd * 1.6)
rabbit.set_physics_process(false)
player.select(7) # an empty hotbar slot
inv.set_slot(7, null)
check(not A.melee(player, rabbit) and rabbit.hp == 10.0, "bare hands can't hurt an animal")
check(rabbit.interact_prompt(player).contains("need a weapon"), "and looking at one says what you need")
inv.add("berries", 2)
_hold(player, "berries")
check(not A.melee(player, rabbit) and rabbit.hp == 10.0, "nor can a handful of berries")
inv.add("torch", 1)
_hold(player, "torch")
check(not A.melee(player, rabbit) and rabbit.hp == 10.0, "nor a torch")
inv.add("arrow", 1)
_hold(player, "arrow")
check(not A.melee(player, rabbit) and rabbit.hp == 10.0, "nor an arrow held in your hand")
inv.remove("arrow", 1)
inv.add("stone_axe", 1)
_hold(player, "stone_axe")
check(A.melee(player, rabbit) and rabbit.hp == 4.0, "an axe works, though it's a poor weapon (hp %.0f)" % rabbit.hp)
rabbit.free()
inv.add("club", 1)
_hold(player, "club")
rabbit = sp.spawn("rabbit", site + fwd * 1.6)
rabbit.set_physics_process(false)
await physics_frame
await physics_frame
cam.rotation.x = -0.5 # look down at it
var tgt: Node = player.hands.melee_target()
check(tgt == rabbit, "the club's sweep finds a rabbit at your feet")
cam.rotation.x = 0.0
var st0: float = sv.stamina
check(A.melee(player, rabbit) and rabbit.is_dead(), "one club blow kills a rabbit")
check(sv.stamina < st0, "swinging costs stamina")
check(rabbit.is_in_group("carcass") and rabbit.collision_layer == 2, "it leaves a carcass you can butcher")
check(not A.melee(player, rabbit), "can't keep hitting a carcass")
var meat0 := inv.count("raw_meat")
check(A.butcher(player, rabbit) and inv.count("raw_meat") == meat0 + 1, "butchering a rabbit gives meat")
await process_frame
check(not is_instance_valid(rabbit), "and the carcass is gone")
var far_rabbit: Node = sp.spawn("rabbit", site + fwd * 9.0)
far_rabbit.set_physics_process(false)
check(not A.melee(player, far_rabbit), "can't club something 9 m away")
far_rabbit.free()
print("boar fight")
boar = sp.spawn("boar", site + fwd * 2.5)
boar.set_physics_process(false)
sv.stamina = 100.0
A.melee(player, boar)
check(boar.state == "charge" and not boar.is_dead(), "hit a boar and it charges (hp %.0f)" % boar.hp)
h0 = sv.health
_sim(boar, 3.0)
check(sv.health <= h0 - 13.0, "and gores you (health %.0f -> %.0f)" % [h0, sv.health])
var hits := 0
while not boar.is_dead() and hits < 12:
sv.stamina = 100.0
boar.global_position = site + fwd * 2.0
A.melee(player, boar)
hits += 1
check(boar.is_dead() and hits >= 3, "a boar takes several blows (%d more)" % hits)
var hide0 := inv.count("hide")
player.global_position = boar.global_position - fwd * 1.5
A.butcher(player, boar)
check(inv.count("hide") > hide0 and inv.count("raw_meat") >= meat0 + 4, "a boar gives hide and plenty of meat")
sv.health = 100.0
player.global_position = site + Vector3(0, 0.02, 0)
print("spear")
inv.add("spear", 1)
_hold(player, "spear")
deer = sp.spawn("deer", site + fwd * 9.0)
deer.set_physics_process(false)
await physics_frame
var a: Array = player.hands._aim()
var aim_at: Vector3 = deer.global_position + Vector3(0, 1.0, 0)
sv.stamina = 100.0
# A thrown spear drops about 0.6 m over 9 m: aim a little high, as a player would.
check(A.throw_spear(player, 0, a[0], aim_at + Vector3(0, 0.6, 0) - (a[0] as Vector3), world) and inv.count("spear") == 0, "threw the spear")
for _i in range(40):
await physics_frame
check(deer.hp < 40.0, "the spear hits the deer (hp %.0f)" % deer.hp)
var spear_drop: Node = null
for d in world.get_node("Dropped").get_children():
if d.item_id == "spear":
spear_drop = d
check(spear_drop != null and spear_drop.global_position.distance_to(deer.global_position) < 2.0, "and lands at its feet")
player.global_position = spear_drop.global_position + Vector3(0.5, 0, 0)
check(A.pickup(player, spear_drop) and inv.count("spear") == 1, "pick the spear up again")
player.global_position = site + Vector3(0, 0.02, 0)
print("bow")
inv.add("bow", 1)
_hold(player, "bow")
check(not A.can_draw(player), "no arrows, no shot")
inv.add("arrow", 3)
check(A.can_draw(player), "with arrows you can draw")
deer.hp = 40.0
a = player.hands._aim()
A.shoot(player, a[0], aim_at - (a[0] as Vector3) + Vector3(0, 0.15, 0), 1.0, world)
for _i in range(30):
await physics_frame
check(inv.count("arrow") == 2, "a shot uses one arrow")
check(deer.hp <= 40.0 - 25.0, "a full draw hits hard (hp %.0f)" % deer.hp)
A.shoot(player, a[0], aim_at - (a[0] as Vector3) + Vector3(0, 0.15, 0), 1.0, world)
for _i in range(30):
await physics_frame
check(deer.is_dead(), "two full-draw arrows bring down a deer")
player.global_position = deer.global_position - fwd * 1.5
var arrows0 := inv.count("arrow")
var hide1 := inv.count("hide")
A.butcher(player, deer)
check(inv.count("arrow") == arrows0 + 2 and inv.count("hide") > hide1, "butchering gives hide and your arrows back")
player.global_position = site + Vector3(0, 0.02, 0)
a = player.hands._aim()
A.shoot(player, a[0], fwd + Vector3(0, -0.25, 0), 0.6, world)
for _i in range(40):
await physics_frame
var stuck: Node = null
for d in world.get_node("Dropped").get_children():
if d.item_id == "arrow":
stuck = d
check(stuck != null and stuck.global_position.distance_to(site) < 15.0, "an arrow that misses sticks in the ground")
world.weather.force("clear")
print("cooking and buffs")
var fire: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), site - fwd * 2.0 + Vector3(0, terrain.sample_height(site.x, site.z) - site.y, 0)))
player.global_position = site + Vector3(0, 0.02, 0)
check(A.craft(player, "cooked_meat") and inv.count("cooked_meat") == 1, "cooked meat at the campfire")
sv.health = 2.0
inv.add("raw_meat", 1)
_hold(player, "raw_meat")
sv.hunger = 50.0
A.eat(player, 0)
check(sv.health >= 1.0 and sv.health <= 2.0, "raw meat can hurt, but never kills (health %.0f)" % sv.health)
sv.health = 60.0
sv.sick = 0.0 # (raw meat may have made you ill: tests/test_food.gd covers that)
_hold(player, "cooked_meat")
A.eat(player, 0)
check(sv.buffs.has("well_fed") and sv.buff_mult("stamina_regen") > 1.2, "cooked meat makes you Well fed")
sv._update_statuses()
check(sv.statuses.any(func(s): return s.begins_with("Well fed")), "and the HUD shows it (%s)" % [sv.statuses])
# Healing with the buff vs without (fed and warm, no recent hits).
sv.hunger = 90.0
sv.warmth = 70.0
sv._hurt_cd = 0.0
sv.health = 50.0
sv.step(10.0)
var fed_gain: float = sv.health - 50.0
sv.buffs.clear()
sv.health = 50.0
sv.step(10.0)
var plain_gain: float = sv.health - 50.0
check(fed_gain > plain_gain * 1.8, "Well fed heals faster (%.1f vs %.1f per 10 s)" % [fed_gain, plain_gain])
sv.damage(5.0, site)
var hh: float = sv.health
sv.step(3.0)
check(sv.health <= hh, "no natural healing just after a hit")
world.remove_piece(fire)
print("cloak")
var bench: Node = world.spawn_piece(DB.get_item("workbench"), Transform3D(Basis(), site + fwd * -3.0 + Vector3(0, -0.02, 0)))
inv.add("hide", 4)
inv.add("fiber", 4)
check(A.craft(player, "hide_cloak") and inv.count("hide_cloak") == 1, "a hide cloak from 4 hide and 4 fiber")
world.remove_piece(bench)
world.day_night.time = 0.0
world.day_night.apply(true)
sv.warmth = 50.0
for _i in range(300):
sv.step(1.0)
var bare: float = sv.warmth
_hold(player, "hide_cloak")
check(A.wear(player, 0) and player.worn == "hide_cloak" and inv.count("hide_cloak") == 0, "right-click wears it")
sv.warmth = 50.0
for _i in range(300):
sv.step(1.0)
check(sv.warmth > bare + 12.0, "the cloak keeps you warmer at night (%.0f vs %.0f)" % [sv.warmth, bare])
check(A.take_off(player) and player.worn == "" and inv.count("hide_cloak") == 1, "take it off again")
_hold(player, "hide_cloak")
A.wear(player, 0)
world.day_night.time = 0.5
world.day_night.apply(true)
inv.add("cooked_meat", 1)
_hold(player, "cooked_meat")
sv.hunger = 40.0
A.eat(player, 0)
print("spawner")
for k in sp._cooldown:
sp._cooldown[k] = 0.0 # (the kills above put every kind on its respawn timer)
sp.enabled = true
sp._check = 0.0
sp._process(0.1)
check(sp.alive().size() >= 1, "the spawner tops the wildlife back up")
var stray: Node = sp.spawn("deer", player.global_position + Vector3(0, 0, 0))
stray.position = player.global_position + Vector3(110, 0, 0)
sp._check = 0.0
sp._process(0.1)
check(stray.is_queued_for_deletion(), "animals that wander far away despawn")
sp.enabled = false
await process_frame
_clear(sp)
# Walk in from the lake's east side to the last dry spot before the water,
# then check the deer refuses to step in.
var lake: Vector2 = terrain.lake_center
var swimmer: Node = sp.spawn("deer", Vector3(lake.x + terrain.lake_radius + 8.0, 0, lake.y))
var edge := Vector3.INF
var x: float = lake.x + terrain.lake_radius + 8.0
while x > lake.x:
if terrain.sample_height(x - 1.0, lake.y) < terrain.water_level + 0.1:
edge = Vector3(x, terrain.sample_height(x, lake.y), lake.y)
break
x -= 0.25
swimmer.position = edge if edge != Vector3.INF else swimmer.position
check(edge != Vector3.INF and swimmer._blocked(Vector3(-1, 0, 0)), "animals won't walk into the lake")
check(not swimmer._blocked(Vector3(1, 0, 0)), "but will walk away from it")
swimmer.free()
print("save + load")
var buff_left: float = sv.buffs.get("well_fed", {}).get("left", 0.0)
var stuck_pos: Vector3 = stuck.global_position
world.save_now()
world.queue_free()
await process_frame
await process_frame
world = await _spawn_world()
player = world.get_node("Player")
sv = player.survival
check(player.worn == "hide_cloak", "still wearing the cloak after loading")
check(sv.buffs.has("well_fed") and absf(float(sv.buffs.well_fed.left) - buff_left) < 2.0, "Well fed survives a reload")
var stuck2: Node = null
for d in world.get_node("Dropped").get_children():
if d.item_id == "arrow" and d.global_position.distance_to(stuck_pos) < 0.05:
stuck2 = d
check(stuck2 != null, "the stuck arrow is still there")
check(world.creatures.alive().size() >= 5, "a loaded world has wildlife again (%d)" % world.creatures.alive().size())
print("death")
player.set_physics_process(false)
sv.set_process(false)
sv.damage(1000.0, player.global_position)
var grave: Node = null
for p in world.get_node("Pieces").get_children():
if p.item_id == "grave":
grave = p
check(player.worn == "" and grave != null and grave.inventory.count("hide_cloak") == 1, "dying puts your cloak in the grave")
world.queue_free()
_finished = true