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:
@@ -1,13 +1,24 @@
|
||||
extends "res://scripts/building/piece.gd"
|
||||
## Campfire: flickering light, flames, embers and smoke. A "campfire" station
|
||||
## (cooking arrives in M1b along with fuel and warmth).
|
||||
## Campfire: light, warmth and cooking, while it has fuel.
|
||||
##
|
||||
## Starts with a little kindling. E adds wood (or sticks) from your inventory;
|
||||
## it burns down in play time and goes out at zero. While lit it is a
|
||||
## "campfire" station (cooking) and a heat source (warmth within ~7 m).
|
||||
|
||||
const MAX_FUEL := 900.0 ## 15 min
|
||||
const START_FUEL := 120.0
|
||||
const HEAT_RADIUS := 7.0
|
||||
|
||||
var fuel := START_FUEL
|
||||
var _light: OmniLight3D
|
||||
var _fx: Array[CPUParticles3D] = []
|
||||
var _t := 0.0
|
||||
var _noise := FastNoiseLite.new()
|
||||
var _lit := true
|
||||
|
||||
func _build() -> void:
|
||||
add_to_group("station:campfire")
|
||||
add_to_group("heat")
|
||||
_noise.frequency = 2.5
|
||||
_light = OmniLight3D.new()
|
||||
_light.light_color = Color(1.0, 0.62, 0.3)
|
||||
@@ -18,30 +29,76 @@ func _build() -> void:
|
||||
_light.position = Vector3(0, 0.7, 0)
|
||||
add_child(_light)
|
||||
|
||||
add_child(_particles("Flames", 26, 0.55, Vector3(0, 0.12, 0), 0.5, 1.0, 0.5, 0.9, _gradient([
|
||||
_fx.append(_particles("Flames", 26, 0.55, Vector3(0, 0.12, 0), 0.5, 1.0, 0.5, 0.9, _gradient([
|
||||
[0.0, Color(1.0, 0.8, 0.35, 0.0)], [0.15, Color(1.0, 0.62, 0.2, 0.75)],
|
||||
[0.6, Color(0.95, 0.3, 0.06, 0.45)], [1.0, Color(0.5, 0.08, 0.02, 0.0)]]), true, 0.32, 0.12))
|
||||
add_child(_particles("Embers", 10, 2.0, Vector3(0, 0.3, 0), 0.8, 2.0, 0.5, 1.0, _gradient([
|
||||
_fx.append(_particles("Embers", 10, 2.0, Vector3(0, 0.3, 0), 0.8, 2.0, 0.5, 1.0, _gradient([
|
||||
[0.0, Color(1.0, 0.75, 0.35, 1.0)], [1.0, Color(1.0, 0.3, 0.05, 0.0)]]), true, 0.04, 0.15))
|
||||
var smoke := _particles("Smoke", 10, 5.0, Vector3(0, 1.0, 0), 0.6, 1.1, 0.6, 1.4, _gradient([
|
||||
[0.0, Color(0.55, 0.54, 0.52, 0.0)], [0.25, Color(0.58, 0.57, 0.55, 0.07)],
|
||||
[1.0, Color(0.7, 0.7, 0.7, 0.0)]]), false, 0.9, 0.15)
|
||||
smoke.spread = 25.0
|
||||
smoke.gravity = Vector3(0.35, 0.25, 0.1) # drifts with the wind
|
||||
# TODO(M1b): smoke renders as an opaque dark ball whatever its ramp colour
|
||||
# (alpha-blended unshaded particles; additive flames are fine). Disabled
|
||||
# until that's fixed, likely by moving to a small particle shader.
|
||||
smoke.queue_free()
|
||||
for p in _fx:
|
||||
add_child(p)
|
||||
# TODO: smoke. Alpha-blended unshaded particles rendered as an opaque dark
|
||||
# ball whatever their ramp colour (additive flames are fine). Needs a small
|
||||
# particle shader.
|
||||
_set_lit(fuel > 0.0)
|
||||
|
||||
func display_name() -> String:
|
||||
return "Campfire"
|
||||
|
||||
func is_lit() -> bool:
|
||||
return _lit
|
||||
|
||||
func station_active() -> bool:
|
||||
return _lit
|
||||
|
||||
## 0..1 warmth this fire gives at `pos`.
|
||||
func heat_at(pos: Vector3) -> float:
|
||||
if not _lit:
|
||||
return 0.0
|
||||
return 1.0 - smoothstep(1.5, HEAT_RADIUS, pos.distance_to(global_position))
|
||||
|
||||
func add_fuel(seconds: float) -> void:
|
||||
fuel = minf(fuel + seconds, MAX_FUEL)
|
||||
_set_lit(true)
|
||||
|
||||
func interact_prompt(player: Node) -> String:
|
||||
var has_fuel: bool = player.inventory.count("wood") + player.inventory.count("stick") > 0
|
||||
if _lit:
|
||||
var left := "%d:%02d" % [int(fuel) / 60, int(fuel) % 60]
|
||||
return "Campfire (%s left)%s" % [left, " [E] Add fuel" if has_fuel else " (add wood or sticks)"]
|
||||
return "Campfire (out)%s" % (" [E] Add fuel to light" if has_fuel else " (needs wood or sticks)")
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
Actions.add_fuel(player, self)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _lit:
|
||||
return
|
||||
fuel -= delta
|
||||
if fuel <= 0.0:
|
||||
fuel = 0.0
|
||||
_set_lit(false)
|
||||
return
|
||||
_t += delta
|
||||
var f := _noise.get_noise_1d(_t * 10.0)
|
||||
_light.light_energy = 2.2 + f * 0.7
|
||||
# Burns lower in its last minute.
|
||||
var strength := clampf(fuel / 60.0, 0.35, 1.0)
|
||||
_light.light_energy = (2.2 + f * 0.7) * strength
|
||||
_light.position = Vector3(f * 0.05, 0.7 + f * 0.05, 0)
|
||||
|
||||
func _set_lit(on: bool) -> void:
|
||||
_lit = on
|
||||
if _light != null:
|
||||
_light.visible = on
|
||||
for p in _fx:
|
||||
p.emitting = on
|
||||
|
||||
func save_extra() -> Dictionary:
|
||||
return {"fuel": fuel}
|
||||
|
||||
func load_extra(d: Dictionary) -> void:
|
||||
fuel = float(d.get("fuel", fuel))
|
||||
_set_lit(fuel > 0.0)
|
||||
|
||||
static func _gradient(stops: Array) -> Gradient:
|
||||
var g := Gradient.new()
|
||||
g.offsets = PackedFloat32Array(stops.map(func(s): return s[0]))
|
||||
@@ -61,7 +118,7 @@ static func _soft_dot() -> GradientTexture2D:
|
||||
t.height = 64
|
||||
return t
|
||||
|
||||
func _particles(label: String, amount: int, lifetime: float, pos: Vector3, vmin: float, vmax: float,
|
||||
static func _particles(label: String, amount: int, lifetime: float, pos: Vector3, vmin: float, vmax: float,
|
||||
smin: float, smax: float, ramp: Gradient, additive: bool, quad_size: float, radius: float) -> CPUParticles3D:
|
||||
var p := CPUParticles3D.new()
|
||||
p.name = label
|
||||
@@ -81,7 +138,7 @@ func _particles(label: String, amount: int, lifetime: float, pos: Vector3, vmin:
|
||||
# Flames shrink as they rise.
|
||||
var curve := Curve.new()
|
||||
curve.add_point(Vector2(0.0, 1.0))
|
||||
curve.add_point(Vector2(1.0, 0.35 if additive else 1.6))
|
||||
curve.add_point(Vector2(1.0, 0.35))
|
||||
p.scale_amount_curve = curve
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2.ONE * quad_size
|
||||
|
||||
23
scripts/building/grave.gd
Normal file
23
scripts/building/grave.gd
Normal file
@@ -0,0 +1,23 @@
|
||||
extends "res://scripts/building/piece.gd"
|
||||
## Grave: left where you died, holding everything you carried. Open it with E
|
||||
## to take your things back; it crumbles once empty.
|
||||
|
||||
var inventory := Inventory.new(40)
|
||||
|
||||
func _build() -> void:
|
||||
inventory.changed.connect(_on_changed)
|
||||
|
||||
func display_name() -> String:
|
||||
return "Your grave"
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
return "Your grave [E] Recover belongings"
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
player.open_inventory(self)
|
||||
|
||||
func _on_changed() -> void:
|
||||
for s in inventory.slots:
|
||||
if s != null:
|
||||
return
|
||||
queue_free.call_deferred()
|
||||
1
scripts/building/grave.gd.uid
Normal file
1
scripts/building/grave.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ok0js1e4pb4s
|
||||
Reference in New Issue
Block a user