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()