Files
Cinderwild/tests/test_weather_building.gd

390 lines
17 KiB
GDScript

extends SceneTree
## M1c: weather, shelter, snap building + support + dismantling, doors, beds
## and sleeping, resource regrowth, and saving all of it.
## Run: godot --headless --path . --script res://tests/test_weather_building.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 _settle() -> void:
for _i in range(3):
await physics_frame
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("Huts", 1337)
S.current_id = id
_weather_model()
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)
## The weather model on its own: deterministic and with sensible odds.
func _weather_model() -> void:
print("weather model")
var dn: Node = load("res://scripts/world/day_night.gd").new()
var wa: Node = load("res://scripts/world/weather.gd").new()
var wb: Node = load("res://scripts/world/weather.gd").new()
wa.setup(1337, dn)
wb.setup(1337, dn)
var same := true
var counts := {}
var cold_days := 0
var snow_on_warm_day := false
var days := 60
var samples := 0
var t := 0.0
while t < days * 1200.0:
var s: String = wa.state_at(t)
if s != wb.state_at(t):
same = false
var day := int(t / 1200.0) + 1
if not wa.hard_winter(day): # (the mix over ordinary days; hard winters lean snowy)
counts[s] = counts.get(s, 0) + 1
samples += 1
if s in ["snow", "blizzard"] and not wa.is_cold_day(day):
snow_on_warm_day = true
t += 20.0
var ordinary := 0
for d in range(1, days + 1):
if wa.hard_winter(d):
continue # (P4's hard winters are always cold: tests/test_hazards.gd)
ordinary += 1
if wa.is_cold_day(d):
cold_days += 1
var total := float(samples)
var frac := func(names: Array) -> float:
var n := 0
for k in names:
n += counts.get(k, 0)
return n / total
check(same, "same seed and time give the same weather")
check(frac.call(["clear"]) > 0.3 and frac.call(["clear"]) < 0.5, "clear about 40%% of the time (%.0f%%)" % (frac.call(["clear"]) * 100))
var wet: float = frac.call(["rain", "storm", "snow", "blizzard"])
check(wet > 0.2 and wet < 0.4, "rain or snow about 30%% of the time (%.0f%%)" % (wet * 100))
var hard: float = frac.call(["storm", "blizzard"])
check(hard > 0.04 and hard < 0.16, "storms about 10%% of the time (%.0f%%)" % (hard * 100))
check(cold_days >= ordinary * 0.12 and cold_days <= ordinary * 0.4, "about a quarter of ordinary days are cold (%d of %d)" % [cold_days, ordinary])
check(not snow_on_warm_day, "it only snows in the valley on cold days")
check(wa.state_at(0.0) == "clear" and wa.state_at(500.0) == "clear", "a new world starts with clear weather")
var other: Node = load("res://scripts/world/weather.gd").new()
other.setup(99, dn)
var differs := false
t = 0.0
while t < 20 * 1200.0 and not differs:
differs = other.state_at(t) != wa.state_at(t)
t += 60.0
check(differs, "a different seed gives different weather")
check(wa.snows_at(Vector3(0, wa.snowline + 1.0, 0)), "above the snowline it snows")
for n in [dn, wa, wb, other]:
n.free()
func _body(world: Node3D) -> void:
var player: CharacterBody3D = world.get_node("Player")
var sv: Node = player.survival
var dn: Node = world.day_night
var wx: Node = world.weather
var bs: Node = world.build
var inv: Inventory = player.inventory
var terrain: Node3D = world.get_node("Terrain")
var props: Node3D = world.get_node("Props")
player.set_physics_process(false)
sv.set_process(false)
wx.set_process(false)
wx.force("clear")
# A flat-ish spot a little way from the spawn, facing the same way.
var fwd := -player.global_basis.z
var site: Vector3 = bs.debug_find_site(player.global_position + fwd * 12.0, 3.0)
var yaw: float = player.rotation.y
print("snapping")
var floor_def: Dictionary = DB.get_item("wood_floor")
var tb: Node = terrain.get_node("TerrainBody")
var r0: Dictionary = bs.placement(floor_def, {"position": site, "normal": Vector3.UP, "collider": tb}, yaw)
check(r0.ok, "a floor can go on open ground (%s)" % r0.reason)
var f0: Node3D = world.spawn_piece(floor_def, r0.xf)
await _settle()
check(f0.grounded and f0.support == 1.0, "a floor on the ground is fully supported")
var right: Vector3 = f0.global_basis.x
var aim := f0.global_position + right * 1.6
var r1: Dictionary = bs.placement(floor_def, {"position": aim, "normal": Vector3.UP, "collider": f0}, yaw + 0.2)
var off: Vector3 = r1.xf.origin - f0.global_position
check(r1.ok and r1.snapped and off.distance_to(right * 2.0) < 0.01, "the next floor snaps exactly 2 m along (%s)" % off)
check(absf(r1.xf.basis.get_euler().y - f0.global_basis.get_euler().y) < 0.01, "and lines up with the first despite a skewed aim")
var wall_def: Dictionary = DB.get_item("wood_wall")
var aimw := f0.global_position - f0.global_basis.z * 0.9
var rw: Dictionary = bs.placement(wall_def, {"position": aimw, "normal": Vector3.UP, "collider": f0}, yaw)
var edge := f0.global_position - f0.global_basis.z * 1.0
check(rw.ok and rw.xf.origin.distance_to(edge) < 0.01, "a wall snaps onto the floor's edge")
var air := site + Vector3(0, 6, 0)
var ra: Dictionary = bs.placement(floor_def, {"position": air, "normal": Vector3.UP, "collider": null}, yaw)
check(not ra.ok and ra.reason == "Not enough support", "a floor in mid-air has no support (%s)" % ra.reason)
var rblock: Dictionary = bs.placement(floor_def, {"position": f0.global_position, "normal": Vector3.UP, "collider": f0}, yaw)
check(not rblock.ok or rblock.xf.origin.distance_to(f0.global_position) > 1.0, "can't place a floor on top of another one")
world.remove_piece(f0)
await _settle()
print("hut")
var hut: Dictionary = await bs.debug_build_hut(site, yaw)
await _settle()
check(hut.placed == 20 and hut.failed.is_empty(), "built a 20-piece hut by snapping (%d placed, failed: %s, blocker %s)" % [hut.placed, hut.failed, bs.last_blocker])
var weakest := 1.0
for p in hut.pieces:
weakest = minf(weakest, p.support)
check(weakest >= bs.MIN_SUPPORT, "every hut piece is supported (weakest %.2f)" % weakest)
var inside: Vector3 = hut.pieces[0].global_position + hut.pieces[0].global_basis * Vector3(1, 0.05, 1)
var outside: Vector3 = site + fwd * -6.0
outside.y = terrain.sample_height(outside.x, outside.z)
check(bs.roofed_at(inside) and bs.enclosure_at(inside) > 0.99 and bs.sheltered_at(inside), "inside the hut is sheltered")
check(not bs.roofed_at(outside) and not bs.sheltered_at(outside), "outside is not")
print("rain and shelter")
dn.time = 0.5
dn.apply(true)
wx.force("rain")
player.global_position = outside
sv.wet = 0.0
sv._shelter_left = 0.0
sv.step(0.5)
check(sv.wet > 0.0 and "Wet" in sv.statuses, "rain soaks you outside")
player.global_position = inside
sv.wet = 0.0
sv._shelter_left = 0.0
sv.step(0.5)
check(sv.wet == 0.0 and sv.sheltered and "Sheltered" in sv.statuses, "inside the hut you stay dry")
check(world.air_warmth(inside, true) > world.air_warmth(outside, false) + 8.0, "the hut keeps out the weather's chill")
var fire_out: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), outside + right * 3.0))
var fire_in: Node = world.spawn_piece(DB.get_item("campfire"), Transform3D(Basis(), inside + right * 0.2))
check(fire_out.burn_rate() > 2.0 and fire_in.burn_rate() == 1.0, "rain eats an open fire's fuel, not one under a roof (%.1fx)" % fire_out.burn_rate())
sv._rest_build = 0.0
for _i in range(25):
sv.step(1.0)
check(sv.rested > 0.0 and "Rested" in sv.statuses, "resting by a fire in the hut makes you Rested")
world.remove_piece(fire_out)
wx.force("storm")
wx.step(30.0)
check(wx.lightning_count > 0, "storms bring lightning")
wx.force("snow")
player.global_position = outside
sv.wet = 0.0
sv._shelter_left = 0.0
sv.step(0.5)
check(sv.wet == 0.0, "snow makes you cold, not wet")
check(world.air_warmth(outside) < world.air_warmth(inside, true) - 10.0, "snow is colder outside than in the hut")
wx.force("clear")
print("door")
var door: Node = null
for p in hut.pieces:
if p.item_id == "wood_door":
door = p
player.global_position = door.global_position + door.global_basis.z * 1.5
var panel_ray := func() -> bool:
var from: Vector3 = door.global_position + door.global_basis.z * 0.6 + Vector3(0, 1.0, 0)
var q := PhysicsRayQueryParameters3D.create(from, from - door.global_basis.z * 1.2, 1)
return not world.get_world_3d().direct_space_state.intersect_ray(q).is_empty()
check(panel_ray.call(), "a closed door blocks the doorway")
check(A.toggle_door(player, door) and door.open, "E opens the door")
door.set_open(true, false)
await _settle()
check(not panel_ray.call(), "an open door lets you through")
door.set_open(false, false)
await _settle()
print("support and collapse")
# A lone wall on a floor, with floors stacked off its top as an overhang:
# they reach out a few pieces, then run out of support.
var s2: Vector3 = bs.debug_find_site(site + right * 16.0, 6.0) - fwd * 4.0
s2.y = terrain.sample_height(s2.x, s2.z)
var rg: Dictionary = bs.placement(floor_def, {"position": s2, "normal": Vector3.UP, "collider": tb}, yaw)
var g: Node3D = world.spawn_piece(floor_def, rg.xf)
await physics_frame
var rtw: Dictionary = bs.placement(wall_def, {"position": g.global_position - g.global_basis.z * 0.9, "normal": Vector3.UP, "collider": g}, yaw)
check(rg.ok and rtw.ok, "a test wall on its own floor (%s %s)" % [rg.reason, rtw.reason])
var tw: Node3D = world.spawn_piece(wall_def, rtw.xf)
await physics_frame
var out_dir: Vector3 = -tw.global_basis.z
var chain: Array = []
var top: Vector3 = tw.global_position + Vector3(0, 2, 0)
var stopped := -1
var stop_reason := ""
for i in range(8):
var rc: Dictionary = bs.placement(floor_def, {"position": top + out_dir * (1.0 + 2.0 * i), "normal": Vector3.UP, "collider": tw}, yaw)
if not rc.ok:
stopped = i
stop_reason = rc.reason
break
chain.append(world.spawn_piece(floor_def, rc.xf))
await physics_frame
check(stopped >= 2 and stopped <= 5 and stop_reason == "Not enough support", "an overhang reaches %d floors before it runs out of support (%s %s)" % [stopped, stop_reason, bs.last_blocker])
if chain.size() >= 2:
check(chain[-1].support < chain[0].support, "support falls off along the overhang (%.2f -> %.2f)" % [chain[0].support, chain[-1].support])
inv.load_data(Inventory.new(inv.size()).to_data())
player.global_position = tw.global_position + out_dir * -1.5
var n_chain := chain.size()
check(A.dismantle(player, tw), "X dismantles a wall")
await _settle()
var gone := 0
for p in chain:
if not is_instance_valid(p) or p.get_parent() == null:
gone += 1
check(gone == n_chain and n_chain > 0, "the overhang it held up collapses (%d of %d)" % [gone, n_chain])
check(inv.count("wood_wall") == 1 and inv.count("wood_floor") == n_chain, "dismantled and collapsed pieces come back to you")
world.remove_piece(g)
# Taking one wall out of the hut: the roof is tied in by the other walls,
# the ridge and the gables, so nothing falls.
var hut_wall: Node3D = hut.pieces[4] # north wall, west half (see debug_build_hut)
var hut_floor: Node3D = hut.pieces[0]
player.global_position = inside
check(hut_wall.item_id == "wood_wall" and A.dismantle(player, hut_wall), "dismantled one hut wall")
await _settle()
var roofs_left := 0
for p in world.get_node("Pieces").get_children():
if p.item_id == "wood_roof":
roofs_left += 1
check(roofs_left == 4, "the roof still stands (%d of 4 roof pieces)" % roofs_left)
_hold(player, "wood_wall")
var aimb: Vector3 = hut_floor.global_position - hut_floor.global_basis.z * 0.9
var rb: Dictionary = bs.placement(wall_def, {"position": aimb, "normal": Vector3.UP, "collider": hut_floor}, yaw)
check(rb.ok and A.place(player, 0, rb.xf, world), "and it goes back where it was (%s)" % rb.reason)
await _settle()
check(bs.sheltered_at(inside), "the hut is whole again")
world.remove_piece(fire_in)
var chest: Node = world.spawn_piece(DB.get_item("chest"), Transform3D(Basis(Vector3.UP, yaw), inside - right * 1.0))
chest.inventory.add("stone", 3)
check(not A.dismantle(player, chest), "can't dismantle a chest with things in it")
print("bed and sleep")
inv.add("bed", 1)
_hold(player, "bed")
var bed_xf := Transform3D(Basis(Vector3.UP, yaw), inside + right * 0.9 - Vector3(0, 0.05, 0))
check(world.placement_valid(DB.get_item("bed"), bed_xf), "a bed fits on the hut floor")
A.place(player, 0, bed_xf, world)
var bed: Node = null
for p in world.get_node("Pieces").get_children():
if p.item_id == "bed":
bed = p
player.global_position = bed.global_position + right * 1.0
check(A.use_bed(player, bed) and world.has_bed_spawn and bed.is_spawn(), "E on a bed sets your spawn point")
dn.time = 14.0 / 24.0
check(not A.use_bed(player, bed), "can't sleep in the afternoon")
dn.time = 22.0 / 24.0
var day0: int = dn.day
var hunger0: float = sv.hunger
check(A.use_bed(player, bed), "can sleep at 22:00 in a sheltered bed")
check(dn.day == day0 + 1 and absf(dn.hours() - 6.0) < 0.05, "woke at 06:00 the next day (%s)" % dn.clock_text())
check(sv.rested > 0.0 and sv.hunger < hunger0 and sv.hunger > hunger0 - 15.0, "rested, and a little hungrier (%.0f -> %.0f)" % [hunger0, sv.hunger])
sv.hunger = 0.0
sv.health = 0.3
player.global_position = outside
sv.step(2.0)
check(player.global_position.distance_to(bed.global_position) < 2.0, "after dying you wake at your bed")
print("regrowth")
var tree: Node = null
for uid in props.resource_nodes:
var n = props.resource_nodes[uid]
if n.kind == "tree" and not n.depleted and n.global_position.distance_to(player.global_position) > 15.0:
tree = n
break
inv.add("stone_axe", 1)
_hold(player, "stone_axe")
player.global_position = tree.global_position + Vector3(1.5, 0, 0)
var hits := 0
while not tree.depleted and hits < 20:
A.harvest_hit(player, tree, tree.global_position + Vector3(0, 1, 0))
sv.stamina = 100.0
hits += 1
check(tree.depleted and props.regrow.has(tree.uid), "a felled tree starts a regrowth timer (%.0f s)" % props.regrow.get(tree.uid, 0.0))
var pk: Node = null
for uid in props.pickups:
if not props.pickups[uid].taken:
pk = props.pickups[uid]
break
player.global_position = pk.global_position + Vector3(0.5, 0, 0)
A.pickup(player, pk)
check(pk.taken and props.regrow.has(pk.uid), "a picked-up item starts one too")
props.skip_time(1000.0)
check(pk.taken and props.regrow.has(pk.uid), "with you standing on the spot, the item waits")
player.global_position = outside + Vector3(0, 0, 40)
props.skip_time(2500.0)
check(not tree.depleted and tree.is_in_group("harvestable") and tree.collision_layer == 1, "the tree grows back after two days")
check(not pk.taken and pk.visible, "the item comes back")
print("save + load")
tree.restore(0.0)
props.regrow[tree.uid] = 777.0
var tree_uid: String = tree.uid
door.set_open(true, false)
var n_pieces := world.get_node("Pieces").get_child_count()
var bed_pos: Vector3 = bed.global_position
world.save_now()
world.queue_free()
await process_frame
await process_frame
world = await _spawn_world()
player = world.get_node("Player")
props = world.get_node("Props")
await _settle()
check(world.get_node("Pieces").get_child_count() == n_pieces, "all %d pieces restored" % n_pieces)
var weakest2 := 1.0
var door2: Node = null
for p in world.get_node("Pieces").get_children():
if p.is_in_group("build"):
weakest2 = minf(weakest2, p.support)
if p.item_id == "wood_door":
door2 = p
check(weakest2 >= world.build.MIN_SUPPORT, "nothing collapses on load (weakest %.2f)" % weakest2)
check(door2 != null and door2.open, "the open door is still open")
check(world.has_bed_spawn and world.bed_spawn.distance_to(bed_pos) < 0.05, "spawn point restored")
check(absf(float(props.regrow.get(tree_uid, 0.0)) - 777.0) < 2.0, "regrowth timers restored")
check(world.build.sheltered_at(inside), "the restored hut still shelters you")
# Older saves had no regrowth timers: anything gone starts a fresh one.
var state: Dictionary = S.load_state(S.current_id)
state.erase("regrow")
props.regrow.clear()
props.restore_regrow(state.get("regrow", {}))
check(props.regrow.has(tree_uid) and props.regrow[tree_uid] == props.REGROW_SECONDS["tree"], "old saves: gone things get a full timer")
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)