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

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}