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 ## Food buffs: id -> {name, left (s), stamina_regen, health_regen (multipliers)}. var buffs: Dictionary = {} var _hurt_cd := 0.0 ## no natural healing for a few seconds after a hit signal hurt(amount: float, from: Vector3) const HURT_NO_REGEN := 6.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 wear: Dictionary = ItemDB.get_item(player.worn).get("wear", {}) if not player.worn.is_empty() else {} var target: float = world.air_warmth(p, sheltered) + heat * 70.0 + float(wear.get("warmth", 0.0)) if wet > 0.0: target -= 22.0 * float(wear.get("wet_chill", 1.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) * buff_mult("stamina_regen") * dt, MAX) for id in buffs.keys(): buffs[id].left -= dt if buffs[id].left <= 0.0: buffs.erase(id) _hurt_cd = maxf(_hurt_cd - dt, 0.0) 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 and _hurt_cd <= 0.0: health = minf(health + 0.5 * buff_mult("health_regen") * 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") for id in buffs: statuses.append("%s %d:%02d" % [buffs[id].name, int(buffs[id].left) / 60, int(buffs[id].left) % 60]) ## 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) # Raw meat can hurt a little, but never kills you. health = clampf(health + float(food.get("health", 0)), minf(health, 1.0), MAX) if food.has("buff"): add_buff(food.buff) 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 buffs.clear() changed.emit() ## Takes a hit (a boar, later wolves). Pauses natural healing briefly. func damage(amount: float, from: Vector3) -> void: if dead: return health = maxf(health - amount, 0.0) _hurt_cd = HURT_NO_REGEN hurt.emit(amount, from) changed.emit() if health <= 0.0: dead = true died.emit() ## Multiplier from food buffs for one effect (1.0 = none). Buffs don't stack: ## the strongest applies. func buff_mult(key: String) -> float: var m := 1.0 for id in buffs: m = maxf(m, float(buffs[id].get(key, 1.0))) return m func add_buff(b: Dictionary) -> void: var id: String = b.get("id", "buff") buffs[id] = {"name": b.get("name", id), "left": float(b.get("seconds", 300.0)), "stamina_regen": float(b.get("stamina_regen", 1.0)), "health_regen": float(b.get("health_regen", 1.0))} _update_statuses() func to_data() -> Dictionary: return {"health": health, "stamina": stamina, "hunger": hunger, "warmth": warmth, "wet": wet, "rested": rested, "buffs": buffs.duplicate(true)} 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)) buffs = (d.get("buffs", {}) as Dictionary).duplicate(true) dead = false changed.emit()