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:
Paul Bailey
2026-11-05 11:49:13 +00:00
parent 657c3140fe
commit 3389ec9a21
27 changed files with 1541 additions and 56 deletions

288
scripts/world/hazards.gd Normal file
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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}