- Health, stamina, hunger, warmth and wet with HUD bars, status tags and clock - 20-minute day/night cycle with moon, stars and lit clouds - Campfire fuel (2 min start, 15 min max), heat radius, cooking needs a lit fire - Berry bushes (regrow 10 min), mushrooms, roasted mushroom, torch - Stamina costs for sprint, jump and harvest hits - Death respawns at island start and leaves a grave bag - Saves include stats, time of day, fire fuel, regrowth timers and graves - New test_survival_stats suite; tests fail if a script error aborts them
190 lines
7.4 KiB
GDScript
190 lines
7.4 KiB
GDScript
extends SceneTree
|
|
## M1b: day/night, hunger, warmth, stamina, campfire fuel + heat + cooking,
|
|
## berry bushes, eating, death + grave, and saving all of it.
|
|
##
|
|
## Fast-forwards time by calling survival.step() / day_night.advance() / the
|
|
## fire's _process() directly, so the whole test runs in seconds.
|
|
## Run: godot --headless --path . --script res://tests/test_survival_stats.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
|
|
|
|
## Advances the simulation `seconds` in 1 s steps (stats + clock + fires).
|
|
func _ff(world: Node3D, seconds: float) -> void:
|
|
var player: Node = world.get_node("Player")
|
|
for _i in range(int(seconds)):
|
|
world.day_night.advance(1.0)
|
|
for f in world.get_tree().get_nodes_in_group("heat"):
|
|
f._process(1.0)
|
|
player.survival.step(1.0)
|
|
|
|
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("Stats", 1337)
|
|
S.current_id = id
|
|
var world: Node3D = await _spawn_world()
|
|
await _body(world, id)
|
|
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 _body(world: Node3D, id: String) -> void:
|
|
var player: CharacterBody3D = world.get_node("Player")
|
|
var sv: Node = player.survival
|
|
var dn: Node = world.day_night
|
|
var inv: Inventory = player.inventory
|
|
var terrain: Node3D = world.get_node("Terrain")
|
|
player.set_physics_process(false) # keep the player put while we fast-forward
|
|
sv.set_process(false)
|
|
|
|
print("day / night")
|
|
check(dn.day == 1 and absf(dn.hours() - 8.0) < 0.1, "new world starts on day 1 at 08:00 (%s)" % dn.clock_text())
|
|
dn.time = 0.5; dn.apply(true)
|
|
check(dn.daylight > 0.9, "noon is bright (daylight %.2f)" % dn.daylight)
|
|
dn.time = 0.0; dn.apply(true)
|
|
check(dn.daylight < 0.05, "midnight is dark (daylight %.2f)" % dn.daylight)
|
|
dn.time = 0.99
|
|
dn.advance(dn.day_length * 0.02)
|
|
check(dn.day == 2, "clock rolls over to day 2")
|
|
|
|
print("hunger and eating")
|
|
dn.time = 0.5; dn.apply(true)
|
|
var h0: float = sv.hunger
|
|
_ff(world, 300)
|
|
check(sv.hunger < h0 - 15.0, "hunger drains over 5 min (%.0f -> %.0f)" % [h0, sv.hunger])
|
|
inv.add("berries", 3)
|
|
_hold(player, "berries")
|
|
var h1: float = sv.hunger
|
|
check(A.eat(player, 0) and sv.hunger > h1 and inv.count("berries") == 2, "eating berries restores food")
|
|
|
|
print("cold and fire")
|
|
dn.time = 21.0 / 24.0; dn.apply(true) # night (fast-forward below stays before dawn)
|
|
sv.warmth = 60.0
|
|
_ff(world, 240)
|
|
check(sv.warmth < 30.0 and "Cold" in sv.statuses, "a night in the open gets cold (warmth %.0f, %s)" % [sv.warmth, sv.statuses])
|
|
var spot := player.global_position + Vector3(2.0, 0, 0)
|
|
spot.y = terrain.sample_height(spot.x, spot.z)
|
|
var fire: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), spot))
|
|
_ff(world, 30)
|
|
check(sv.warmth > 55.0 and "Warm" in sv.statuses, "sitting by the fire warms you (warmth %.0f)" % sv.warmth)
|
|
|
|
print("fuel and cooking")
|
|
inv.add("mushroom", 2)
|
|
check(A.can_craft(player, "roasted_mushroom"), "can roast a mushroom at a lit fire")
|
|
fire._process(fire.fuel + 1.0)
|
|
check(not fire.is_lit() and fire.heat_at(player.global_position) == 0.0, "the fire burns out when its fuel runs out")
|
|
check(not A.can_craft(player, "roasted_mushroom"), "an unlit fire can't cook")
|
|
inv.add("wood", 2)
|
|
check(A.add_fuel(player, fire) and fire.is_lit() and inv.count("wood") == 1, "adding wood relights it")
|
|
check(A.craft(player, "roasted_mushroom") and inv.count("roasted_mushroom") == 1, "roasted a mushroom")
|
|
|
|
print("stamina")
|
|
sv.stamina = 100.0
|
|
var swings := 0
|
|
while sv.use_stamina(6.0) and swings < 100:
|
|
swings += 1
|
|
check(swings >= 14 and swings < 30, "about 16 swings empty your stamina (%d)" % swings)
|
|
_ff(world, 6)
|
|
check(sv.stamina > 90.0, "stamina regenerates after a pause")
|
|
|
|
print("berry bushes")
|
|
var bush: Node3D = world.get_node("Props").forage.values()[0]
|
|
player.global_position = bush.global_position + Vector3(1.0, 0.3, 0)
|
|
var b0 := inv.count("berries")
|
|
check(A.forage(player, bush) and inv.count("berries") > b0 and inv.count("fiber") >= 1, "foraged berries and fiber")
|
|
check(not A.forage(player, bush), "a picked bush is bare")
|
|
bush._process(bush.regrow_seconds + 1.0)
|
|
check(bush.ripe(), "it regrows")
|
|
inv.add("stick", 1)
|
|
inv.add("fiber", 2)
|
|
check(A.craft(player, "torch") and inv.count("torch") == 1, "torch crafted from stick + fiber")
|
|
_hold(player, "torch")
|
|
check(player.hands.held_heat() > 0.0, "a held torch gives a little warmth")
|
|
|
|
print("save + load")
|
|
sv.hunger = 42.0
|
|
sv.warmth = 33.0
|
|
fire.fuel = 321.0
|
|
A.forage(player, bush)
|
|
var regrow_left: float = bush.regrow_left
|
|
dn.day = 3; dn.time = 0.75
|
|
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(absf(sv.hunger - 42.0) < 0.5 and absf(sv.warmth - 33.0) < 0.5, "stats restored (food %.0f, warmth %.0f)" % [sv.hunger, sv.warmth])
|
|
check(world.day_night.day == 3 and absf(world.day_night.hours() - 18.0) < 0.2, "time of day restored (%s)" % world.day_night.clock_text())
|
|
var fire2: Node = null
|
|
for p in world.get_node("Pieces").get_children():
|
|
if p.item_id == "campfire":
|
|
fire2 = p
|
|
check(fire2 != null and absf(fire2.fuel - 321.0) < 2.0 and fire2.is_lit(), "campfire fuel restored")
|
|
var bush2: Node = world.get_node("Props").forage[bush.uid] if is_instance_valid(bush) else world.get_node("Props").forage.values()[0]
|
|
check(absf(bush2.regrow_left - regrow_left) < 2.0, "bush regrow timer restored")
|
|
|
|
print("death and grave")
|
|
player.set_physics_process(false)
|
|
sv.set_process(false)
|
|
player.inventory.add("wood", 9)
|
|
player.inventory.add("stone_axe", 1)
|
|
var death_pos := player.global_position
|
|
sv.hunger = 0.0
|
|
sv.health = 0.5
|
|
sv.step(2.0)
|
|
check(sv.health == 50.0 and not sv.dead, "you respawn with half health")
|
|
check(player.inventory.count("wood") == 0, "your inventory went into the grave")
|
|
var grave: Node = null
|
|
for p in world.get_node("Pieces").get_children():
|
|
if p.item_id == "grave":
|
|
grave = p
|
|
check(grave != null and grave.inventory.count("wood") >= 9 and grave.inventory.count("stone_axe") == 1, "the grave holds your things")
|
|
check(grave != null and grave.global_position.distance_to(death_pos) < 15.0, "grave is where you fell")
|
|
player.global_position = grave.global_position + Vector3(1, 0.5, 0)
|
|
for i in range(grave.inventory.size()):
|
|
if grave.inventory.get_slot(i) != null:
|
|
A.transfer(player, grave.inventory, i, player.inventory, grave)
|
|
await process_frame
|
|
check(player.inventory.count("stone_axe") == 1, "recovered your belongings")
|
|
check(not is_instance_valid(grave) or grave.is_queued_for_deletion(), "the empty grave crumbles")
|
|
world.queue_free()
|
|
_finished = true
|
|
|
|
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)
|