M1b: survival stats, day/night, fire fuel, cooking, foraging, torches, death + grave
- 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
This commit is contained in:
@@ -8,6 +8,7 @@ extends SceneTree
|
||||
## Run: godot --headless --path . --script res://tests/test_save_load.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var A: Node # Actions
|
||||
var S: Node # Saves
|
||||
var DB: Node # ItemDB
|
||||
@@ -114,9 +115,11 @@ func _run() -> void:
|
||||
check(chest2 != null and chest2.inventory.count("flint") == 7 and chest2.inventory.count("wood") == 12, "chest rebuilt with its contents")
|
||||
check(S.read_meta(id).play_time >= 0.0, "meta updated")
|
||||
|
||||
_finished = true
|
||||
world.queue_free()
|
||||
S.delete_world(id)
|
||||
DirAccess.remove_absolute(S.root_dir)
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ extends SceneTree
|
||||
## Run: godot --headless --path . --script res://tests/test_survival_core.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false ## set at the very end; a script error aborts before it
|
||||
var _actions: Node
|
||||
var _db: Node
|
||||
|
||||
@@ -28,6 +29,7 @@ func _run() -> void:
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
await _test_world(world)
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
@@ -134,6 +136,7 @@ func _test_world(world: Node3D) -> void:
|
||||
var moving: int = inv.get_slot(wood_slot).count
|
||||
_actions.transfer(player, inv, wood_slot, chest.inventory, chest)
|
||||
check(chest.inventory.count("wood") == moving, "shift-click stored %d wood in the chest" % moving)
|
||||
_finished = true
|
||||
|
||||
func ItemDB_get(_p, id: String) -> Dictionary:
|
||||
return _db.get_item(id)
|
||||
|
||||
189
tests/test_survival_stats.gd
Normal file
189
tests/test_survival_stats.gd
Normal file
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user