M1b: survival stats, day/night, fire fuel, cooking, foraging, torches, death + grave
- Health, stamina, hunger, warmth and wet with HUD bars, status tags and clock - 20-minute day/night cycle with moon, stars and lit clouds - Campfire fuel (2 min start, 15 min max), heat radius, cooking needs a lit fire - Berry bushes (regrow 10 min), mushrooms, roasted mushroom, torch - Stamina costs for sprint, jump and harvest hits - Death respawns at island start and leaves a grave bag - Saves include stats, time of day, fire fuel, regrowth timers and graves - New test_survival_stats suite; tests fail if a script error aborts them
This commit is contained in:
127
scripts/player/survival.gd
Normal file
127
scripts/player/survival.gd
Normal file
@@ -0,0 +1,127 @@
|
||||
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
|
||||
|
||||
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 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)
|
||||
var target: float = world.air_warmth(p) + 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 * 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")
|
||||
|
||||
## 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
|
||||
|
||||
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}
|
||||
|
||||
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))
|
||||
dead = false
|
||||
changed.emit()
|
||||
Reference in New Issue
Block a user