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:
Paul Bailey
2026-11-04 01:19:41 +00:00
parent 2b1afc86ba
commit fc7eb3cfed
28 changed files with 1326 additions and 97 deletions

View File

@@ -13,6 +13,7 @@ extends Node3D
const ProcMesh = preload("res://scripts/world/proc_mesh.gd")
const ResourceNodeScript = preload("res://scripts/world/resource_node.gd")
const PickupScript = preload("res://scripts/world/pickup.gd")
const ForageScript = preload("res://scripts/world/forage.gd")
const CELL := 48.0
const GRASS_TILE := 12.0
@@ -40,6 +41,7 @@ var _resources: Node3D ## harvestable trees/boulders (each its own body)
## creation order gives the same id to the same tree on every load.
var resource_nodes: Dictionary = {} ## uid -> resource node
var pickups: Dictionary = {} ## uid -> pickup
var forage: Dictionary = {} ## uid -> berry bush (forage.gd)
var _bark_mat: StandardMaterial3D
var _leaf_mat: ShaderMaterial
@@ -500,8 +502,9 @@ func _scatter_pickups() -> void:
add_child(root)
var rng := RandomNumberGenerator.new()
rng.seed = world_seed + 6
var want := {"stick": 260, "stone": 170, "flint": 80}
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25}
# Order matters: new kinds go at the END so existing saves keep their ids.
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110}
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25}
for id: String in want:
var mesh := ItemDB.mesh_for(id)
var placed := 0
@@ -525,6 +528,8 @@ func _scatter_pickups() -> void:
"flint":
var shore := 1.0 - smoothstep(terrain.water_level + 0.5, terrain.water_level + 4.0, h)
chance = 0.06 + shore * 0.9 + (1.0 - n.y) * 2.0
"mushroom":
chance = _forest(x, z) * 0.7 # deep forest only
# A few right by the spawn so the first craft is quick.
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0:
chance += 0.5
@@ -537,6 +542,48 @@ func _scatter_pickups() -> void:
root.add_child(pk)
pk.global_transform = Transform3D(_ground_basis(n, 1.0, yaw, 1.0), Vector3(x, h - 0.02, z))
placed += 1
_scatter_berry_bushes(root)
## Berry bushes along forest edges and in meadows, plus a couple near spawn.
func _scatter_berry_bushes(root: Node3D) -> void:
var rng := RandomNumberGenerator.new()
rng.seed = world_seed + 7
var placed := 0
var tries := 0
while placed < 70 and tries < 8000:
tries += 1
var x := rng.randf_range(-terrain.play_radius, terrain.play_radius)
var z := rng.randf_range(-terrain.play_radius, terrain.play_radius)
var yaw := rng.randf() * TAU
var keep := rng.randf()
if not terrain.inside_boundary(x, z, 4.0):
continue
var h: float = terrain.sample_height(x, z)
var n: Vector3 = terrain.sample_normal(x, z)
if h < terrain.water_level + 0.6 or n.y < 0.85:
continue
var f := _forest(x, z)
var chance := 0.12 + (1.0 - absf(f - 0.45) * 2.0) * 0.5
var near_spawn := Vector2(x - _spawn.x, z - _spawn.z).length()
if near_spawn < 6.0:
continue
if near_spawn < 25.0:
chance += 0.4
if keep > chance:
continue
var bush: Area3D = ForageScript.new()
bush.uid = "b%d" % forage.size()
bush.rng.seed = hash(bush.uid) ^ world_seed
bush.setup(_leaf_mat_static())
root.add_child(bush)
bush.global_transform = Transform3D(_ground_basis(n, 0.5, yaw, rng.randf_range(0.9, 1.2)), Vector3(x, h - 0.05, z))
forage[bush.uid] = bush
placed += 1
## Berry-bush meshes carry their own vertex colours (including red berries),
## so they use the plain item material rather than the wind foliage shader.
func _leaf_mat_static() -> Material:
return ItemDB.item_material()
# --- grass streaming --------------------------------------------------------