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:
288
scripts/world/hazards.gd
Normal file
288
scripts/world/hazards.gd
Normal file
@@ -0,0 +1,288 @@
|
||||
extends Node3D
|
||||
## Weather hazards (P4, docs/PROGRESSION_DESIGN.md §5) that need more than
|
||||
## the weather model:
|
||||
##
|
||||
## - Hard winters (weather.gd hard_winter()): a warning the day before and
|
||||
## on the day; the lake's edge freezes over (walkable ice you can't fish
|
||||
## through), on every machine since it comes from the shared clock.
|
||||
## - Lightning: in a storm, every minute or so a bolt hits a tree near
|
||||
## someone, preferring tall trees near the rim. It burns for a while (a
|
||||
## fire you can warm yourself by), then falls, leaving charcoal. Decided:
|
||||
## trees only for now; buildings are never struck.
|
||||
##
|
||||
## Strikes and warnings happen where the game decides (single-player, a
|
||||
## server); players' games are told (world_net.gd _s2c_strike).
|
||||
|
||||
const Weather = preload("res://scripts/world/weather.gd")
|
||||
|
||||
const STRIKE_MIN := 50.0
|
||||
const STRIKE_MAX := 110.0
|
||||
const STRIKE_RANGE := 90.0 ## trees this near someone can be struck
|
||||
const BURN_SECONDS := 40.0
|
||||
const CHARCOAL := 2
|
||||
const ICE_IN := 0.62 ## the ice: from this x lake radius out to the shore
|
||||
const ICE_OUT := 1.7
|
||||
|
||||
var world: Node3D
|
||||
var deciding := true ## false on a player's game online
|
||||
var strikes := 0
|
||||
var _strike_left := 30.0
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _burning: Dictionary = {} ## resource node -> [fire node, seconds left]
|
||||
var _ice: MeshInstance3D
|
||||
var _ice_body: StaticBody3D
|
||||
var _warned := {} ## day -> true
|
||||
|
||||
func setup(w: Node3D, decides: bool) -> void:
|
||||
world = w
|
||||
deciding = decides
|
||||
_rng.seed = w.terrain.noise_seed + 1201
|
||||
_build_ice()
|
||||
w.day_night.new_day.connect(_on_new_day)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
step(delta)
|
||||
|
||||
func step(dt: float) -> void:
|
||||
_set_ice(world.weather.in_hard_winter())
|
||||
_snow_left -= dt
|
||||
if _snow_left <= 0.0:
|
||||
_snow_left = 2.0
|
||||
_update_roof_snow()
|
||||
for n in _burning.keys():
|
||||
var e: Array = _burning[n]
|
||||
e[1] = float(e[1]) - dt
|
||||
if float(e[1]) <= 0.0 or not is_instance_valid(n):
|
||||
_burn_out(n)
|
||||
if not deciding:
|
||||
return
|
||||
var w: Node = world.weather
|
||||
if w.state == "storm" and w.precip > 0.7:
|
||||
_strike_left -= dt
|
||||
if _strike_left <= 0.0:
|
||||
_strike_left = _rng.randf_range(STRIKE_MIN, STRIKE_MAX)
|
||||
var t := pick_tree()
|
||||
if t != null:
|
||||
strike(t)
|
||||
|
||||
# --- lightning -------------------------------------------------------------------------
|
||||
|
||||
## A standing tree near someone, the taller and nearer the rim the likelier.
|
||||
func pick_tree() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_s := -INF
|
||||
for n in world.props.resource_nodes.values():
|
||||
if n.kind != "tree" or n.depleted or _burning.has(n):
|
||||
continue
|
||||
var near := false
|
||||
for p in world.players():
|
||||
if (n as Node3D).global_position.distance_to(p.global_position) < STRIKE_RANGE:
|
||||
near = true
|
||||
break
|
||||
if not near:
|
||||
continue
|
||||
var pos: Vector3 = n.global_position
|
||||
var s: float = world.props._near_rim(pos.x, pos.z) * 2.0 + n.base_xf.basis.get_scale().y + _rng.randf() * 1.5
|
||||
if s > best_s:
|
||||
best_s = s
|
||||
best = n
|
||||
return best
|
||||
|
||||
## Lightning hits tree `n`: a flash, a crack, and it's on fire.
|
||||
func strike(n: Node3D) -> void:
|
||||
strikes += 1
|
||||
show_strike(n)
|
||||
if world.server_mode:
|
||||
WorldNet.struck(n)
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(n.global_position) < 60.0:
|
||||
Actions.tell(p, "Lightning struck a tree! It's burning.")
|
||||
|
||||
## What everyone sees: the flash, and the tree alight.
|
||||
func show_strike(n: Node3D) -> void:
|
||||
world.weather.flash = 1.0
|
||||
if _burning.has(n):
|
||||
return
|
||||
var fire := TreeFire.new()
|
||||
fire.name = "TreeFire"
|
||||
add_child(fire)
|
||||
fire.global_position = n.global_position
|
||||
fire.build(DisplayServer.get_name() != "headless")
|
||||
_burning[n] = [fire, BURN_SECONDS]
|
||||
|
||||
func burning(n: Node3D) -> bool:
|
||||
return _burning.has(n)
|
||||
|
||||
## Burnt through: the tree falls (where the game decides; players' games
|
||||
## see it fall via world_net), leaving charcoal at its foot.
|
||||
func _burn_out(n: Node3D) -> void:
|
||||
var e: Array = _burning[n]
|
||||
_burning.erase(n)
|
||||
if is_instance_valid(e[0]):
|
||||
(e[0] as Node).queue_free()
|
||||
if not deciding or not is_instance_valid(n) or n.depleted:
|
||||
return
|
||||
var top: Vector3 = n.global_position + Vector3(0, 2.0, 0)
|
||||
var from: Vector3 = n.global_position + Vector3(1, 0, 0)
|
||||
n._show_hit(0.0, top, from)
|
||||
if world.server_mode:
|
||||
WorldNet.node_hit(n, top, from)
|
||||
world.drop_item("charcoal", CHARCOAL, Transform3D(Basis(), n.global_position + Vector3(0.8, 0.3, 0.4)))
|
||||
|
||||
## A burning tree's fire: flames up the trunk, a light, and warmth.
|
||||
class TreeFire extends Node3D:
|
||||
const RADIUS := 6.0
|
||||
func build(visual: bool) -> void:
|
||||
add_to_group("heat")
|
||||
if not visual:
|
||||
return
|
||||
var light := OmniLight3D.new()
|
||||
light.light_color = Color(1.0, 0.55, 0.25)
|
||||
light.light_energy = 3.0
|
||||
light.omni_range = 14.0
|
||||
light.position = Vector3(0, 3.0, 0)
|
||||
add_child(light)
|
||||
# The campfire's soft additive flames, bigger and up the trunk.
|
||||
var CF: Script = load("res://scripts/building/campfire.gd")
|
||||
var ramp: Gradient = CF._gradient([[0.0, Color(1.0, 0.8, 0.35, 0.0)], [0.15, Color(1.0, 0.62, 0.2, 0.8)],
|
||||
[0.6, Color(0.95, 0.3, 0.06, 0.5)], [1.0, Color(0.5, 0.08, 0.02, 0.0)]])
|
||||
for i in range(3):
|
||||
var p: CPUParticles3D = CF._particles("Flames%d" % i, 30, 1.0, Vector3(0, 1.2 + 1.6 * i, 0), 0.8, 2.0,
|
||||
0.6, 1.2, ramp, true, 0.9, 0.7 - 0.15 * i)
|
||||
add_child(p)
|
||||
p.emitting = true
|
||||
func heat_at(pos: Vector3) -> float:
|
||||
return 1.0 - smoothstep(2.0, RADIUS, pos.distance_to(global_position))
|
||||
|
||||
# --- hard winters ---------------------------------------------------------------------
|
||||
|
||||
func _on_new_day(day: int) -> void:
|
||||
if not deciding or _warned.has(day):
|
||||
return
|
||||
_warned[day] = true
|
||||
if Weather.hard_winter(day + 1) and not Weather.hard_winter(day):
|
||||
Actions.tell_all("The air has a bite to it. A hard winter is coming tomorrow: three bitter days. Stock up on food and firewood.")
|
||||
elif Weather.hard_winter(day) and not Weather.hard_winter(day - 1):
|
||||
Actions.tell_all("A hard winter has set in. The lake's edge has frozen, and the animals are holed up.")
|
||||
elif Weather.hard_winter(day - 1) and not Weather.hard_winter(day):
|
||||
Actions.tell_all("The hard winter is over.")
|
||||
|
||||
## A ring of ice round the lake, at the water line: it shows (and you can
|
||||
## walk on it) wherever the water is, from the shore out to ICE_IN of the
|
||||
## lake's radius. Off except in a hard winter.
|
||||
func _build_ice() -> void:
|
||||
var t: Node3D = world.terrain
|
||||
var c: Vector2 = t.lake_center
|
||||
var r0: float = t.lake_radius * ICE_IN
|
||||
var r1: float = t.lake_radius * ICE_OUT
|
||||
var y: float = t.water_level + 0.04
|
||||
var segs := 64
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
var faces := PackedVector3Array()
|
||||
var nz := FastNoiseLite.new()
|
||||
nz.seed = t.noise_seed + 1202
|
||||
for i in range(segs):
|
||||
var a0 := TAU * i / segs
|
||||
var a1 := TAU * (i + 1) / segs
|
||||
var k0 := 1.0 + 0.12 * nz.get_noise_1d(i * 7.0)
|
||||
var k1 := 1.0 + 0.12 * nz.get_noise_1d((i + 1) * 7.0)
|
||||
var p := [Vector3(c.x + cos(a0) * r0 * k0, y, c.y + sin(a0) * r0 * k0), Vector3(c.x + cos(a0) * r1, y, c.y + sin(a0) * r1),
|
||||
Vector3(c.x + cos(a1) * r1, y, c.y + sin(a1) * r1), Vector3(c.x + cos(a1) * r0 * k1, y, c.y + sin(a1) * r0 * k1)]
|
||||
for tri in [[p[0], p[2], p[1]], [p[0], p[3], p[2]]]:
|
||||
for v in tri:
|
||||
st.set_normal(Vector3.UP)
|
||||
st.add_vertex(v)
|
||||
faces.append(v)
|
||||
_ice = MeshInstance3D.new()
|
||||
_ice.name = "LakeIce"
|
||||
_ice.mesh = st.commit()
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = Color(0.86, 0.93, 0.98, 0.96)
|
||||
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
m.roughness = 0.35
|
||||
m.metallic_specular = 0.8
|
||||
_ice.material_override = m
|
||||
_ice.visible = false
|
||||
add_child(_ice)
|
||||
_ice_body = StaticBody3D.new()
|
||||
_ice_body.name = "IceBody"
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := ConcavePolygonShape3D.new()
|
||||
shape.set_faces(faces)
|
||||
shape.backface_collision = true
|
||||
cs.shape = shape
|
||||
_ice_body.add_child(cs)
|
||||
_ice_body.collision_layer = 0
|
||||
add_child(_ice_body)
|
||||
|
||||
func _set_ice(on: bool) -> void:
|
||||
if _ice.visible == on:
|
||||
return
|
||||
_ice.visible = on
|
||||
_ice_body.collision_layer = 1 if on else 0
|
||||
|
||||
# --- snow on roofs ----------------------------------------------------------------------
|
||||
|
||||
var _snow_left := 0.0
|
||||
var _snow_mat: StandardMaterial3D
|
||||
var _snow_mesh: BoxMesh
|
||||
|
||||
## Snow piles up on roofs as it settles (weather.gd snow_cover), and melts
|
||||
## off with it. Just a look: a white layer on each roof piece, thicker the
|
||||
## more has settled. (Visual only, so not on a headless server.)
|
||||
func _update_roof_snow() -> void:
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return
|
||||
var cover: float = world.weather.snow_cover
|
||||
if _snow_mesh == null:
|
||||
var BP: Script = load("res://scripts/building/build_piece.gd")
|
||||
_snow_mesh = BoxMesh.new()
|
||||
_snow_mesh.size = Vector3(2.04, 0.12, sqrt(5.0) + 0.25)
|
||||
_snow_mat = StandardMaterial3D.new()
|
||||
_snow_mat.albedo_color = Color(0.93, 0.95, 0.98)
|
||||
_snow_mat.roughness = 0.9
|
||||
_snow_mesh.material = _snow_mat
|
||||
_snow_angle = BP.ROOF_ANGLE
|
||||
_snow_t = BP.ROOF_T
|
||||
for p in world.build.build_pieces():
|
||||
if p.kind != "roof":
|
||||
continue
|
||||
var s: MeshInstance3D = p.get_node_or_null("Snow")
|
||||
if cover < 0.2:
|
||||
if s != null:
|
||||
s.visible = false
|
||||
continue
|
||||
if s == null:
|
||||
s = MeshInstance3D.new()
|
||||
s.name = "Snow"
|
||||
s.mesh = _snow_mesh
|
||||
p.add_child(s)
|
||||
var k := clampf((cover - 0.2) / 0.8, 0.0, 1.0)
|
||||
var n := Vector3(0, cos(_snow_angle), sin(_snow_angle))
|
||||
var along := Vector3(0, sin(_snow_angle), -cos(_snow_angle))
|
||||
s.visible = true
|
||||
s.transform = Transform3D(Basis(Vector3.RIGHT, _snow_angle).scaled(Vector3(1.0, maxf(k, 0.15), 1.0)),
|
||||
along * (sqrt(5.0) * 0.5 - 0.1) + n * (_snow_t + 0.11 + 0.06 * k))
|
||||
|
||||
var _snow_angle := 0.4636476
|
||||
var _snow_t := 0.12
|
||||
|
||||
## How many roofs have snow on them right now (tests).
|
||||
func snowy_roofs() -> int:
|
||||
var n := 0
|
||||
for p in world.build.build_pieces():
|
||||
var s: Node = p.get_node_or_null("Snow")
|
||||
if s != null and s.visible:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func lake_frozen() -> bool:
|
||||
return _ice.visible
|
||||
|
||||
## Frozen over at (x, z): ice, not open water.
|
||||
func iced(x: float, z: float) -> bool:
|
||||
if not lake_frozen():
|
||||
return false
|
||||
var c: Vector2 = world.terrain.lake_center
|
||||
return Vector2(x - c.x, z - c.y).length() > world.terrain.lake_radius * ICE_IN * 0.9
|
||||
Reference in New Issue
Block a user