P4 hazards: food spoilage with rot speed setting, smoking rack and salt, food poisoning, harsher blizzards, hard winters with lake ice, lightning on trees, snow on roofs (protocol 11)
This commit is contained in:
289
tests/test_hazards.gd
Normal file
289
tests/test_hazards.gd
Normal file
@@ -0,0 +1,289 @@
|
||||
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("")
|
||||
Reference in New Issue
Block a user