Files
Emberwild/scripts/player/survival.gd
Paul Bailey fed96ef118 M1c: weather, snap building, shelter, beds and regrowth
- Seeded weather (clear/cloudy/rain/storm, snow and blizzards on cold days
  and above the snowline), lightning, synthesised rain/wind/thunder sound
- Rain/snow particles stopped by roofs; wet ground, settled snow and wind
  via global shader uniforms
- Snap-together wood floor, wall, door, roof and gable with structural
  support, collapse and X to dismantle
- Shelter detection: dry under a roof, warmer and Rested inside with a fire;
  rain burns an uncovered campfire faster
- Bed sets the spawn point and lets you sleep through the night
- Trees, boulders and ground pickups regrow; timers saved
- Grass cleared under floors; shorter HUD hint
- New test_weather_building suite (62 checks) and tests/run_all.sh, which
  also fails on script errors
2026-11-04 10:16:41 +00:00

166 lines
5.0 KiB
GDScript

extends Node
## Survival stats: health, stamina, hunger (satiety) and warmth, each 0..100.
##
## - Hunger drains over about a day and a quarter, faster when you're cold.
## - Warmth moves toward a target: air temperature (day/night, altitude),
## plus heat from fires and a held torch, minus being wet.
## - Health drains while starving or freezing, and regenerates when fed and warm.
## - Stamina pays for sprinting, jumping and swinging, and regenerates after a
## short pause.
##
## step(dt) holds all the rules, so tests can fast-forward time. In M2 this runs
## on the server.
signal changed
signal died
const MAX := 100.0
const HUNGER_DRAIN := 100.0 / 1500.0 ## full to empty in ~1.25 in-game days
const STAMINA_REGEN := 24.0
const STAMINA_DELAY := 1.0
const WET_SECONDS := 90.0
const REST_AFTER := 20.0 ## seconds sheltered by a fire before you're Rested
const REST_SECONDS := 300.0 ## Rested: stamina regenerates 50% faster
var health := MAX
var stamina := MAX
var hunger := MAX ## satiety: 100 = full, 0 = starving
var warmth := 70.0
var wet := 0.0 ## seconds of "Wet" left
var heat := 0.0 ## 0..1 from nearby fires etc (last step)
var statuses: Array[String] = []
var dead := false
var roofed := false ## a roof overhead (rain can't reach you)
var sheltered := false ## roof and walls (the weather barely reaches you)
var rested := 0.0 ## seconds of Rested left
var _shelter_left := 0.0
var _rest_build := 0.0
var player: CharacterBody3D
var world: Node3D
var _stamina_wait := 0.0
func _process(delta: float) -> void:
if world != null and not world.menu_mode:
step(delta)
func step(dt: float) -> void:
if dead:
return
var p := player.global_position
heat = world.heat_at(p)
_shelter_left -= dt
if _shelter_left <= 0.0:
_shelter_left = 0.25
roofed = world.build.roofed_at(p)
sheltered = roofed and world.build.enclosure_at(p) >= 0.74
# Rain soaks you unless there's a roof over your head.
if not roofed and world.weather.rain_at(p) > 0.25:
wet = WET_SECONDS
# Resting: a while by a fire with a roof and walls around you.
if sheltered and heat > 0.25:
_rest_build += dt
if _rest_build >= REST_AFTER:
rested = maxf(rested, REST_SECONDS)
else:
_rest_build = 0.0
rested = maxf(rested - dt, 0.0)
var target: float = world.air_warmth(p, sheltered) + heat * 70.0
if wet > 0.0:
target -= 22.0
wet = maxf(wet - dt * (3.0 if heat > 0.3 else 1.0), 0.0) # fires dry you off
target = clampf(target, 0.0, MAX)
# Warm up faster than you cool down: getting to a fire should feel good.
warmth = move_toward(warmth, target, (7.0 if target > warmth else 1.6) * dt)
hunger = maxf(hunger - HUNGER_DRAIN * dt * (1.5 if warmth < 30.0 else 1.0), 0.0)
if _stamina_wait > 0.0:
_stamina_wait -= dt
else:
stamina = minf(stamina + STAMINA_REGEN * (1.5 if rested > 0.0 else 1.0) * dt, MAX)
var dmg := 0.0
if hunger <= 0.0:
dmg += 0.6
if warmth < 12.0:
dmg += 0.5
if dmg > 0.0:
health = maxf(health - dmg * dt, 0.0)
elif hunger > 40.0 and warmth > 35.0:
health = minf(health + 0.5 * dt, MAX)
_update_statuses()
changed.emit()
if health <= 0.0:
dead = true
died.emit()
func _update_statuses() -> void:
statuses.clear()
if heat > 0.25:
statuses.append("Warm")
if warmth < 12.0:
statuses.append("Freezing")
elif warmth < 30.0:
statuses.append("Cold")
if hunger <= 0.0:
statuses.append("Starving")
elif hunger < 25.0:
statuses.append("Hungry")
if wet > 0.0:
statuses.append("Wet")
if sheltered:
statuses.append("Sheltered")
if rested > 0.0:
statuses.append("Rested")
## Spends stamina for an action. Returns false (and spends nothing) if you're
## too tired. Continuous costs (sprinting) pass `partial = true`.
func use_stamina(amount: float, partial: bool = false) -> bool:
if stamina <= 0.0 or (not partial and stamina < amount * 0.5):
return false
stamina = maxf(stamina - amount, 0.0)
_stamina_wait = STAMINA_DELAY
return true
func eat(food: Dictionary) -> void:
hunger = minf(hunger + float(food.get("hunger", 0)), MAX)
health = minf(health + float(food.get("health", 0)), MAX)
changed.emit()
func soak() -> void:
wet = WET_SECONDS
## After a night's sleep: a little hungrier, dry, warm and rested.
func after_sleep(seconds: float) -> void:
hunger = maxf(hunger - HUNGER_DRAIN * seconds * 0.3, 0.0)
wet = 0.0
warmth = maxf(warmth, 60.0)
stamina = MAX
rested = REST_SECONDS * 2.0
_update_statuses()
changed.emit()
func reset_after_death() -> void:
dead = false
health = 50.0
stamina = MAX
hunger = 50.0
warmth = 60.0
wet = 0.0
changed.emit()
func to_data() -> Dictionary:
return {"health": health, "stamina": stamina, "hunger": hunger, "warmth": warmth, "wet": wet, "rested": rested}
func load_data(d: Dictionary) -> void:
health = float(d.get("health", health))
stamina = float(d.get("stamina", stamina))
hunger = float(d.get("hunger", hunger))
warmth = float(d.get("warmth", warmth))
wet = float(d.get("wet", 0.0))
rested = float(d.get("rested", 0.0))
dead = false
changed.emit()