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
|
||||
@@ -611,9 +611,9 @@ func _scatter_pickups() -> void:
|
||||
rng.seed = world_seed + 6
|
||||
# Order matters: new kinds go at the END so existing saves keep their ids.
|
||||
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70, "fiber": 150,
|
||||
"iron_nugget": 70, "clay": 60}
|
||||
"iron_nugget": 70, "clay": 60, "salt": 40}
|
||||
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25, "fiber": 0.45,
|
||||
"iron_nugget": 0.25, "clay": 0.3}
|
||||
"iron_nugget": 0.25, "clay": 0.3, "salt": 0.25}
|
||||
for id: String in want:
|
||||
# Fibre grows as a plant (wild flax) you pick, 2 fibre a clump.
|
||||
var mesh: Mesh = ItemMeshes.build("fiber_plant") if id == "fiber" else ItemDB.mesh_for(id)
|
||||
@@ -655,8 +655,11 @@ func _scatter_pickups() -> void:
|
||||
var by_lake: bool = Vector2(x - lake.x, z - lake.y).length() < terrain.lake_radius * 1.6
|
||||
var shore: float = 1.0 - smoothstep(terrain.water_level + 0.4, terrain.water_level + 1.8, h)
|
||||
chance = shore * 0.95 if by_lake else 0.0
|
||||
"salt":
|
||||
# Crusts on the cliff tops above the sea (P4).
|
||||
chance = 0.95 if terrain.sea_weight(x, z) > 0.6 and _near_rim(x, z) > 0.35 else 0.0
|
||||
# A few right by the spawn so the first craft is quick.
|
||||
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay"]:
|
||||
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay", "salt"]:
|
||||
chance += 0.5
|
||||
if keep > chance:
|
||||
continue
|
||||
|
||||
@@ -11,6 +11,11 @@ extends Node
|
||||
## (and a storm becomes a blizzard). Above the snowline it always snows.
|
||||
## Outputs ease toward the state's targets over about a minute, so fronts
|
||||
## roll in rather than switching.
|
||||
##
|
||||
## P4 (docs/PROGRESSION_DESIGN.md §5): every 12 days a hard winter, three
|
||||
## bitter cold days in a row (hard_winter()), colder still, with more snow and
|
||||
## blizzards. A blizzard cuts visibility right down and its wind strips the
|
||||
## warmth from anyone out in it (blizzard_chill(); a shelter keeps it out).
|
||||
|
||||
signal state_changed(state: String)
|
||||
|
||||
@@ -30,12 +35,18 @@ const TARGETS := {
|
||||
"rain": {"clouds": 0.92, "precip": 0.6, "wind": 0.60, "light": 0.50, "fog": 2.2, "warmth": -6.0},
|
||||
"storm": {"clouds": 1.00, "precip": 1.0, "wind": 1.00, "light": 0.30, "fog": 3.0, "warmth": -12.0},
|
||||
"snow": {"clouds": 0.90, "precip": 0.6, "wind": 0.35, "light": 0.68, "fog": 2.6, "warmth": -14.0},
|
||||
"blizzard": {"clouds": 1.00, "precip": 1.0, "wind": 0.90, "light": 0.45, "fog": 4.5, "warmth": -24.0},
|
||||
"blizzard": {"clouds": 1.00, "precip": 1.0, "wind": 1.00, "light": 0.40, "fog": 7.0, "warmth": -24.0},
|
||||
}
|
||||
const COLD_DAY_WARMTH := -8.0
|
||||
const BLEND_SECONDS := 50.0 ## time for outputs to cross from one state to another
|
||||
const CALM_START := 600.0 ## a new world's first 10 minutes are always clear
|
||||
const COLD_DAY_CHANCE := 0.25
|
||||
const HARD_WINTER_EVERY := 12 ## days 12-14, 24-26, ...
|
||||
const HARD_WINTER_DAYS := 3
|
||||
const HARD_WINTER_WARMTH := -8.0 ## on top of a cold day's
|
||||
const BLIZZARD_CHILL := 18.0 ## more warmth lost out in a blizzard's wind (a hard winter's freezes you by day)
|
||||
## In a hard winter the weather leans wet (more snow and blizzards).
|
||||
const HARD_WINTER_FRONT := -0.18
|
||||
|
||||
var world_seed := 1337
|
||||
var day_night: Node
|
||||
@@ -76,9 +87,22 @@ func setup(seed: int, dn: Node) -> void:
|
||||
func world_seconds() -> float:
|
||||
return (float(day_night.day - 1) + float(day_night.time)) * float(day_night.day_length)
|
||||
|
||||
## Day `day` is one of a hard winter's three bitter days.
|
||||
static func hard_winter(day: int) -> bool:
|
||||
return day >= HARD_WINTER_EVERY and (day - HARD_WINTER_EVERY) % HARD_WINTER_EVERY < HARD_WINTER_DAYS
|
||||
|
||||
## The first day of the next hard winter on or after `day`.
|
||||
static func next_hard_winter(day: int) -> int:
|
||||
var d := maxi(day, HARD_WINTER_EVERY)
|
||||
while not hard_winter(d):
|
||||
d += 1
|
||||
return d
|
||||
|
||||
func is_cold_day(day: int) -> bool:
|
||||
if day <= 1:
|
||||
return false
|
||||
if hard_winter(day):
|
||||
return true
|
||||
return float(absi(hash(Vector2i(world_seed, day))) % 10000) / 10000.0 < COLD_DAY_CHANCE
|
||||
|
||||
## The raw front value at an absolute time (higher = fairer).
|
||||
@@ -100,7 +124,7 @@ func _state_for(v: float, cold: bool) -> String:
|
||||
## The state at an absolute time, ignoring smoothing and overrides.
|
||||
func state_at(t: float) -> String:
|
||||
var day := int(floor(t / float(day_night.day_length))) + 1
|
||||
return _state_for(front_at(t), is_cold_day(day))
|
||||
return _state_for(front_at(t) + (HARD_WINTER_FRONT if hard_winter(day) else 0.0), is_cold_day(day))
|
||||
|
||||
func force(s: String) -> void:
|
||||
override = s
|
||||
@@ -116,7 +140,7 @@ func snap() -> void:
|
||||
wind = tg.wind
|
||||
light = tg.light
|
||||
fog = tg.fog
|
||||
warmth = tg.warmth + (COLD_DAY_WARMTH if cold_day else 0.0)
|
||||
warmth = tg.warmth + _cold_extra()
|
||||
ground_wet = 1.0 if state in ["rain", "storm"] else 0.0
|
||||
snow_cover = 0.7 if state in ["snow", "blizzard"] else (0.25 if cold_day else 0.0)
|
||||
|
||||
@@ -128,7 +152,7 @@ func _pick_state() -> void:
|
||||
if frozen and s.is_empty():
|
||||
s = "clear"
|
||||
if s.is_empty():
|
||||
s = _state_for(front_at(world_seconds()), cold_day)
|
||||
s = _state_for(front_at(world_seconds()) + (HARD_WINTER_FRONT if hard_winter(day_night.day) else 0.0), cold_day)
|
||||
if s != state:
|
||||
state = s
|
||||
state_changed.emit(state)
|
||||
@@ -145,7 +169,7 @@ func step(dt: float) -> void:
|
||||
wind = move_toward(wind, tg.wind, k)
|
||||
light = move_toward(light, tg.light, k * 0.8)
|
||||
fog = move_toward(fog, tg.fog, k * 3.0)
|
||||
warmth = move_toward(warmth, tg.warmth + (COLD_DAY_WARMTH if cold_day else 0.0), k * 20.0)
|
||||
warmth = move_toward(warmth, tg.warmth + _cold_extra(), k * 20.0)
|
||||
# The ground soaks in a couple of minutes of rain and dries over ten.
|
||||
var raining := precip > 0.2 and not snowing_globally()
|
||||
ground_wet = move_toward(ground_wet, 1.0 if raining else 0.0, dt / (120.0 if raining else 600.0))
|
||||
@@ -163,6 +187,22 @@ func step(dt: float) -> void:
|
||||
flash = 1.0
|
||||
lightning_count += 1
|
||||
|
||||
## Extra cold from the day: a cold day, and a hard winter on top.
|
||||
func _cold_extra() -> float:
|
||||
if not cold_day:
|
||||
return 0.0
|
||||
return COLD_DAY_WARMTH + (HARD_WINTER_WARMTH if day_night != null and hard_winter(day_night.day) else 0.0)
|
||||
|
||||
func in_hard_winter() -> bool:
|
||||
return day_night != null and hard_winter(day_night.day)
|
||||
|
||||
## Extra warmth lost by someone out in a blizzard's wind (0 when sheltered or
|
||||
## when it isn't blowing).
|
||||
func blizzard_chill(sheltered: bool) -> float:
|
||||
if sheltered or state != "blizzard":
|
||||
return 0.0
|
||||
return BLIZZARD_CHILL * clampf(precip, 0.0, 1.0)
|
||||
|
||||
func snowing_globally() -> bool:
|
||||
return state in ["snow", "blizzard"]
|
||||
|
||||
@@ -188,4 +228,6 @@ func label() -> String:
|
||||
var s: String = names[state]
|
||||
if cold_day and state in ["clear", "cloudy"]:
|
||||
s += ", cold"
|
||||
if in_hard_winter():
|
||||
s += " (hard winter)"
|
||||
return s
|
||||
|
||||
@@ -26,6 +26,7 @@ const PIECES := {
|
||||
"bed": preload("res://scripts/building/bed.gd"),
|
||||
"kiln": preload("res://scripts/building/processor.gd"),
|
||||
"smelter": preload("res://scripts/building/processor.gd"),
|
||||
"smoker": preload("res://scripts/building/processor.gd"),
|
||||
"forge": preload("res://scripts/building/forge.gd"),
|
||||
"hearth": preload("res://scripts/building/hearth.gd"),
|
||||
"rug": preload("res://scripts/building/rug.gd"),
|
||||
@@ -65,6 +66,7 @@ var bobber: Node3D ## bobber.gd: the cast fishing float, if any
|
||||
var wolves: Node3D ## wolf_pack.gd (null if this seed has no den site)
|
||||
var bears: Node3D ## bear_range.gd: the island's bear (null online as a client)
|
||||
var thieves: Node3D ## thieves.gd: crows and foxes after food left out
|
||||
var hazards: Node3D ## hazards.gd: lightning on trees, hard-winter ice (P4)
|
||||
var barrow: Node3D ## barrow.gd: the burial mound and its guards (scenery; guards where creatures run)
|
||||
var _saved_bear: Dictionary = {} ## its saved state, kept until bears exists (and re-saved if it never does)
|
||||
var net_sync: Node ## net_sync.gd when playing on a server, else null
|
||||
@@ -165,7 +167,7 @@ func air_warmth(pos: Vector3, sheltered: bool = false) -> float:
|
||||
if sheltered:
|
||||
w += ww * 0.25 + 8.0
|
||||
else:
|
||||
w += ww
|
||||
w += ww - weather.blizzard_chill(false) # (P4: the wind cuts through you)
|
||||
return w
|
||||
|
||||
## 0..1 heat from fires and a held torch.
|
||||
@@ -326,6 +328,7 @@ func skip_time(secs: float) -> void:
|
||||
for f in get_tree().get_nodes_in_group("heat"):
|
||||
if f.has_method("burn"):
|
||||
f.burn(secs)
|
||||
spoil_all() # (food goes off overnight too)
|
||||
|
||||
func _on_terrain_ready() -> void:
|
||||
var t0 := Time.get_ticks_msec()
|
||||
@@ -357,6 +360,12 @@ func _on_terrain_ready() -> void:
|
||||
print("[world] scenery placed in %d ms" % (Time.get_ticks_msec() - t0))
|
||||
if menu_mode:
|
||||
return
|
||||
# Weather hazards (P4): lightning on trees, the hard winter's ice. Decided
|
||||
# here unless we're a player's game online (then the server tells us).
|
||||
hazards = preload("res://scripts/world/hazards.gd").new()
|
||||
hazards.name = "Hazards"
|
||||
add_child(hazards)
|
||||
hazards.setup(self, not is_online())
|
||||
if server_mode:
|
||||
if Saves.has_world():
|
||||
var saved: Dictionary = Saves.load_state(Saves.current_id)
|
||||
@@ -376,6 +385,7 @@ func _on_terrain_ready() -> void:
|
||||
_setup_bear()
|
||||
apply_rules()
|
||||
return
|
||||
_apply_world_rot()
|
||||
_respawn()
|
||||
if Saves.has_world():
|
||||
var state: Dictionary = Saves.load_state(Saves.current_id)
|
||||
@@ -430,6 +440,48 @@ func apply_rules() -> void:
|
||||
wolves.pack_bonus = int(R.difficulty("pack"))
|
||||
for p in PlayerNet.proxies.values():
|
||||
p.survival.drain_mult = R.difficulty("drain")
|
||||
Inventory.rot_speed = ROT_SPEEDS.get(R.rot(), 1.0)
|
||||
|
||||
# --- food spoilage (P4, docs/PROGRESSION_DESIGN.md §5) ----------------------------
|
||||
|
||||
const ROT_SPEEDS := {"normal": 1.0, "slow": 0.5, "off": 0.0}
|
||||
const SPOIL_EVERY := 2.0
|
||||
var _spoil_left := 0.0
|
||||
|
||||
## Keeps Inventory's clock (food's "rot" stamps) on world time. Players'
|
||||
## games online run it too (for showing freshness; the server does the rotting).
|
||||
func _tick_clock() -> void:
|
||||
Inventory.day_seconds = float(day_night.day_length)
|
||||
Inventory.clock = weather.world_seconds()
|
||||
|
||||
## The single-player world's rot setting, from its meta.json (a server's
|
||||
## comes from its rules: apply_rules()).
|
||||
func _apply_world_rot() -> void:
|
||||
var r := String(Saves.read_meta(Saves.current_id).get("rot", "normal")) if Saves.has_world() else "normal"
|
||||
Inventory.rot_speed = ROT_SPEEDS.get(r, 1.0)
|
||||
|
||||
## Food rots: in everyone's bags, in chests, graves and racks, and lying on
|
||||
## the ground (where rotten food just goes). Where the game decides (single
|
||||
## player, a server); players' games online are sent the result.
|
||||
func spoil_all() -> void:
|
||||
if net_sync != null or menu_mode:
|
||||
return
|
||||
_tick_clock()
|
||||
for p in players():
|
||||
if p.get("inventory") != null:
|
||||
p.inventory.spoil()
|
||||
for piece in all_pieces():
|
||||
if piece.get("inventory") != null:
|
||||
piece.inventory.spoil()
|
||||
if Inventory.rot_speed <= 0.0:
|
||||
return
|
||||
for d in all_dropped():
|
||||
if Inventory.keeps_days(d.item_id) <= 0.0:
|
||||
continue
|
||||
if not d.extra.has("rot"):
|
||||
d.extra.rot = Inventory.fresh_rot(d.item_id)
|
||||
elif Inventory.clock >= float(d.extra.rot):
|
||||
take_dropped(d)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if menu_mode:
|
||||
@@ -442,6 +494,11 @@ func _process(delta: float) -> void:
|
||||
var lake: Vector2 = terrain.lake_center
|
||||
_menu_cam.look_at(Vector3(lake.x * 0.5, 2.0, lake.y * 0.5), Vector3.UP)
|
||||
return
|
||||
_tick_clock()
|
||||
_spoil_left -= delta
|
||||
if _spoil_left <= 0.0:
|
||||
_spoil_left = SPOIL_EVERY
|
||||
spoil_all()
|
||||
if not Saves.has_world() or get_tree().paused:
|
||||
return
|
||||
_session_time += delta
|
||||
@@ -619,8 +676,8 @@ func add_dropped_entry(e: Dictionary) -> Node3D:
|
||||
if ItemDB.get_item(e.get("item", "")).is_empty():
|
||||
return null
|
||||
var ex: Dictionary = {}
|
||||
if e.get("extra") is Dictionary and e.extra.has("dur"):
|
||||
ex = {"dur": int(e.extra.dur)}
|
||||
if e.get("extra") is Dictionary:
|
||||
ex = Inventory.extra_of(e.extra) # (a worn tool's "dur", food's "rot")
|
||||
var d := _make_dropped(e.item, int(e.get("count", 1)), _array_to_xf(e.xf), ex)
|
||||
if e.has("nid"):
|
||||
d.set_meta("nid", int(e.nid))
|
||||
@@ -781,6 +838,8 @@ func _update_grass_cuts() -> void:
|
||||
func water_at(x: float, z: float) -> Dictionary:
|
||||
var ground: float = terrain.sample_height(x, z)
|
||||
if terrain.inside_boundary(x, z, -2.0) and ground < terrain.water_level:
|
||||
if hazards != null and hazards.iced(x, z):
|
||||
return {} # (frozen over in a hard winter: cast further out)
|
||||
return {"type": "lake", "surface": terrain.water_level, "depth": terrain.water_level - ground}
|
||||
if terrain.sea_weight(x, z) > 0.5 and ground < terrain.sea_level:
|
||||
return {"type": "sea", "surface": terrain.sea_level, "depth": terrain.sea_level - ground}
|
||||
|
||||
Reference in New Issue
Block a user