Files
Cinderwild/tests/test_hazards.gd

290 lines
11 KiB
GDScript

extends SceneTree
## P4 weather hazards (docs/PROGRESSION_DESIGN.md §5): hard winters every 12
## days (warnings, bitter cold, fewer animals, the lake's edge freezing),
## blizzards that freeze you outdoors but not in a hearth room, and
## lightning that sets trees (never buildings) on fire.
## Run: godot --headless --path . --script res://tests/test_hazards.gd
var _fails := 0
var _finished := false
var A: Node
var S: Node
var DB: Node
var _toasts: Array[String] = []
var world: Node3D
var player: CharacterBody3D
var W: Script
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")
W = load("res://scripts/world/weather.gd")
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)
S.current_id = S.create_world("Hazards", 1337)
world = load("res://scenes/world.tscn").instantiate()
root.add_child(world)
for _i in range(5):
await physics_frame
world.creatures.enabled = false
for c in world.creatures.alive():
c.queue_free()
if world.wolves != null:
world.wolves.enabled = false
world.bears.enabled = false
world.thieves.enabled = false
player = world.player
player.set_physics_process(false)
player.survival.set_process(false)
world.day_night.frozen = true
world.weather.set_process(false)
world.hazards.set_physics_process(false)
await _body()
check(_finished, "test ran to the end (no script errors)")
for w in S.list_worlds():
S.delete_world(w.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(day: int, hours: float) -> void:
world.day_night.day = day
world.day_night.time = hours / 24.0
world.day_night.apply(true)
func _toast_has(s: String) -> bool:
for t in _toasts:
if t.contains(s):
return true
return false
func _body() -> void:
_calendar()
await _blizzard()
_animals()
await _ice()
await _lightning()
_finished = true
# --- hard winters --------------------------------------------------------------------------
func _calendar() -> void:
print("hard winters")
var hw := []
for d in range(1, 40):
if W.hard_winter(d):
hw.append(d)
check(hw == [12, 13, 14, 24, 25, 26, 36, 37, 38], "every 12 days, three in a row: %s" % [hw])
check(W.next_hard_winter(3) == 12 and W.next_hard_winter(15) == 24 and W.next_hard_winter(25) == 25, "(the next one from any day)")
var wx: Node = world.weather
check(wx.is_cold_day(12) and wx.is_cold_day(13) and wx.is_cold_day(14), "they're all cold days")
_set_time(13, 12.0)
wx.force("clear")
var bitter: float = wx.warmth
var cold := -1
for d in range(2, 12):
if wx.is_cold_day(d):
cold = d
check(cold > 0, "(an ordinary cold day for comparison: day %d)" % cold)
_set_time(cold, 12.0)
wx.force("clear")
check(bitter < wx.warmth - 5.0, "colder than an ordinary cold day (%.0f against %.0f)" % [bitter, wx.warmth])
_set_time(13, 12.0)
wx.force("clear")
check(wx.label().contains("hard winter"), "the HUD says so: \"%s\"" % wx.label())
var wet := 0
var wet_hw := 0
for i in range(600):
var t := (11.0 + i / 600.0 * 3.0) * 1200.0
var t2 := (2.0 + i / 600.0 * 3.0) * 1200.0
if wx.state_at(t) in ["snow", "blizzard"]:
wet_hw += 1
if wx.state_at(t2) in ["rain", "storm", "snow", "blizzard"]:
wet += 1
check(wet_hw > wet, "more snow in a hard winter (%d%% of the time, against %d%% wet weather on days 3-5)" % [wet_hw / 6, wet / 6])
print("warnings")
_toasts.clear()
world.hazards._on_new_day(11)
check(_toast_has("hard winter is coming"), "the day before: %s" % [_toasts])
_toasts.clear()
world.hazards._on_new_day(12)
check(_toast_has("has set in"), "on the day: %s" % [_toasts])
_toasts.clear()
world.hazards._on_new_day(15)
check(_toast_has("is over"), "and when it's over")
_toasts.clear()
world.hazards._on_new_day(8)
check(_toasts.is_empty(), "(an ordinary day says nothing)")
# --- blizzard ----------------------------------------------------------------------------
func _blizzard() -> void:
print("a blizzard")
var bs: Node = world.build
var sv: Node = player.survival
_set_time(13, 15.0)
var wx: Node = world.weather
wx.force("blizzard")
check(wx.fog >= 6.0 and wx.light < 0.5, "you can barely see (fog x%.1f)" % wx.fog)
var site: Vector3 = bs.debug_find_site(player.global_position - player.global_basis.z * 16.0, 3.0)
var yaw: float = player.rotation.y
var hut: Dictionary = await bs.debug_build_hut(site, yaw, "stone")
for _i in range(3):
await physics_frame
check(hut.placed == 20, "a stone hut (%d pieces)" % hut.placed)
var b := Basis(Vector3.UP, yaw)
var center: Vector3 = hut.pieces[0].global_transform * Vector3(1, 0, 1)
var hearth: Node3D = world.spawn_piece(DB.get_item("stone_hearth"), Transform3D(b, center + b * Vector3(0, 0, -1.25)))
hearth.fuel = hearth.MAX_FUEL
var inside: Vector3 = center + b * Vector3(0.6, 0.05, 0.8)
var outside: Vector3 = site + b * Vector3(9, 0, 9)
outside.y = world.terrain.sample_height(outside.x, outside.z)
player.inventory.load_data(Inventory.new(40).to_data())
# Outside: no fire, no coat.
player.global_position = outside
sv.warmth = 70.0
sv.wet = 0.0
sv._shelter_left = 0.0
for _i in range(15):
sv.step(1.0)
var lost: float = 70.0 - sv.warmth
check(lost > 45.0, "outside, a blizzard strips your warmth: %.0f lost in 15 s" % lost)
for _i in range(30):
sv.step(1.0)
check(sv.warmth < 12.0 and "Freezing" in sv.statuses, "soon you're Freezing (%.0f)" % sv.warmth)
player.worn = "hide_cloak"
for _i in range(40):
sv.step(1.0)
check(sv.warmth >= 12.0 and not "Freezing" in sv.statuses, "a hide cloak keeps you out of Freezing (%.0f)" % sv.warmth)
player.worn = ""
var air_out: float = world.air_warmth(outside, false)
wx.force("snow")
var air_snow: float = world.air_warmth(outside, false)
wx.force("blizzard")
check(air_out < air_snow - 10.0, "the wind makes it far colder than plain snow (%.0f against %.0f)" % [air_out, air_snow])
# Inside the stone hut by the hearth.
player.global_position = inside
sv.warmth = 20.0
sv._shelter_left = 0.0
for _i in range(30):
sv.step(1.0)
check(sv.sheltered and "Warm" in sv.statuses and sv.warmth > 60.0,
"in the hearth room it's Sheltered and Warm: %.0f (%s)" % [sv.warmth, sv.statuses])
hearth._set_lit(false)
sv._shelter_left = 0.0
sv.step(0.5)
check(world.air_warmth(inside, true) > air_out + 30.0, "even with the fire out, walls keep the wind off (%.0f inside, %.0f out)" % [world.air_warmth(inside, true), air_out])
sv.health = 100.0
wx.force("")
for p in hut.pieces:
if is_instance_valid(p):
p.queue_free()
hearth.queue_free()
for _i in range(3):
await physics_frame
func _animals() -> void:
print("animals")
var sp: Node = world.creatures
_set_time(5, 12.0)
var normal: int = sp.winter_budget("deer", 1)
_set_time(13, 12.0)
var winter: int = sp.winter_budget("deer", 1)
check(winter * 2 == normal, "half as many deer in a hard winter (%d, not %d)" % [winter, normal])
check(sp.winter_budget("boar", 1) >= 1, "(but never none)")
func _ice() -> void:
print("the lake freezes")
var hz: Node = world.hazards
var t: Node3D = world.terrain
var c: Vector2 = t.lake_center
_set_time(5, 12.0)
hz.step(0.1)
check(not hz.lake_frozen(), "not on an ordinary day")
# Somewhere in the lake near its edge, and its middle.
var edge := Vector3.INF
for i in range(72):
var a := TAU * i / 72.0
for k in [0.95, 0.9, 0.85, 0.8, 0.75]:
var x: float = c.x + cos(a) * t.lake_radius * k
var z: float = c.y + sin(a) * t.lake_radius * k
if t.sample_height(x, z) < t.water_level - 0.3:
edge = Vector3(x, t.water_level, z)
break
if edge != Vector3.INF:
break
var mid := Vector3(c.x, t.water_level, c.y)
check(edge != Vector3.INF and not world.water_at(edge.x, edge.z).is_empty(), "(open water by the edge, %.0f m out)" % Vector2(edge.x - c.x, edge.z - c.y).length())
_set_time(13, 12.0)
hz.step(0.1)
await physics_frame
await physics_frame
check(hz.lake_frozen(), "in a hard winter it's frozen over at the edge")
check(world.water_at(edge.x, edge.z).is_empty(), "no fishing through the ice there")
check(not world.water_at(mid.x, mid.z).is_empty() or t.sample_height(mid.x, mid.z) >= t.water_level, "but the middle is still open water")
var q := PhysicsRayQueryParameters3D.create(edge + Vector3(0, 3, 0), edge + Vector3(0, -3, 0), 1)
var hit := world.get_world_3d().direct_space_state.intersect_ray(q)
check(not hit.is_empty() and absf(hit.position.y - t.water_level) < 0.1, "and you can walk on the ice")
_set_time(16, 12.0)
hz.step(0.1)
await physics_frame
await physics_frame
hit = world.get_world_3d().direct_space_state.intersect_ray(q)
check(not hz.lake_frozen() and (hit.is_empty() or hit.position.y < t.water_level - 0.2), "after it, the ice is gone")
# --- lightning -------------------------------------------------------------------------------
func _lightning() -> void:
print("lightning")
var hz: Node = world.hazards
var wx: Node = world.weather
_set_time(6, 14.0)
# Stand among trees near the rim.
var trees: Array = world.props.resource_nodes.values().filter(func(n): return n.kind == "tree" and not n.depleted)
trees.sort_custom(func(a, b): return world.props._near_rim(a.global_position.x, a.global_position.z) > world.props._near_rim(b.global_position.x, b.global_position.z))
var at: Vector3 = trees[0].global_position + Vector3(4, 0, 0)
at.y = world.terrain.sample_height(at.x, at.z)
player.global_position = at
wx.force("clear")
hz._strike_left = 0.0
hz.step(0.1)
check(hz.strikes == 0, "no lightning on a clear day")
wx.force("storm")
_toasts.clear()
var before: int = world.all_dropped().filter(func(d): return d.item_id == "charcoal").size()
hz._strike_left = 0.0
hz.step(0.1)
check(hz.strikes == 1 and hz._burning.size() == 1, "in a storm, a bolt hits a tree")
var tree: Node3D = hz._burning.keys()[0]
check(tree.global_position.distance_to(at) < hz.STRIKE_RANGE, "near you (%.0f m away)" % tree.global_position.distance_to(at))
check(_toast_has("Lightning struck"), "you see it (%s)" % [_toasts])
check(world.heat_at(tree.global_position + Vector3(2.5, 0, 0)) > 0.3, "it burns: you can warm yourself by it")
check(not tree.depleted, "still standing while it burns")
for _i in range(41):
hz.step(1.0)
await physics_frame
check(tree.depleted and not hz.burning(tree), "then it falls (%s)" % ("felled" if tree.depleted else "standing"))
var coal: Array = world.all_dropped().filter(func(d): return d.item_id == "charcoal")
check(coal.size() == before + 1 and int(coal.back().amount) == 2 and coal.back().global_position.distance_to(tree.global_position) < 2.0, "leaving 2 charcoal at its foot")
print("a long storm")
var n0: int = hz.strikes
var pieces0: int = world.all_pieces().size()
var wall: Node3D = world.spawn_piece(DB.get_item("wood_wall"), Transform3D(Basis(), at + Vector3(0, 0, 3)))
for _i in range(600):
hz.step(1.0)
var n: int = hz.strikes - n0
check(n >= 4 and n <= 13, "about one strike a minute or so (%d in 10 minutes)" % n)
check(is_instance_valid(wall) and not wall.is_queued_for_deletion() and world.all_pieces().size() == pieces0 + 1, "it never strikes a building")
wx.force("")