Wolves with a den, fishing, and a mixed diet

Wolves
- Den: a boulder cave at the foot of the rim, sited from the seed (away
  from the sea and spawn, mouth toward the valley, clearing in front)
- Grace period: no wolves days 1-2; they move in on day 3 and howl; hunting
  from night 4; pack of 2, 3 from day 6, 4 from day 9; replaced by day
- Wary pack hunters: circle and dart in one at a time; a lit campfire is a
  safe zone, a torch makes them hesitate, walls block bites (line of
  sight); hit one and the pack turns; hurt wolves flee; a lone wolf is
  timid; by day they defend the den; home at dawn
- Glowing eyes, synthesised howls, can't sleep with wolves prowling
- Wolf pelt and Wolf Cloak (+25 warmth)
- Animals take a committed detour around obstacles

Fishing
- Fishing rod (workbench); cast a float onto the lake, 1.5 s to land a bite
- Perch and rarer trout; faster bites at dawn/dusk and in rain; grill them
- Visible fish near the shore that jump with splash rings

Mixed diet
- Foods belong to meat, fish or greens; each level runs down over ~1.5 days
- One group low: no healing; two low: Malnourished (health drains);
  all three good: Balanced diet (heal x1.5); HUD diet bars; saved

Tests: test_wolves and test_fishing_diet; run_all.sh covers 9 suites
This commit is contained in:
Paul Bailey
2026-11-04 11:32:16 +00:00
parent 67bf85523d
commit f174221268
23 changed files with 2069 additions and 21 deletions

View File

@@ -34,6 +34,11 @@ const KINDS := {
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"damage": 14.0, "attack_range": 1.7, "attack_cd": 1.6, "aggro": 18.0,
"drops": {"raw_meat": [3, 4], "hide": [1, 2]}},
# Wolves are driven by wolf.gd + wolf_pack.gd; "notice" is unused.
"wolf": {"name": "Wolf", "hp": 45.0, "walk": 1.6, "run": 8.6, "turn": 6.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
"damage": 9.0, "attack_range": 1.8, "attack_cd": 1.2,
"drops": {"raw_meat": [1, 2], "wolf_pelt": [1, 1]}},
}
const CARCASS_SECONDS := 300.0
@@ -60,6 +65,13 @@ var _phase := 0.0
var _visual: Node3D
var _legs: Array[Node3D] = []
var _flash := 0.0
var _detour_t := 0.0 ## > 0: following a detour round an obstacle
var _detour_side := 1.0
## Sets the heading, unless the animal is in the middle of a detour.
func steer(y: float) -> void:
if _detour_t <= 0.0:
target_yaw = y
var _mat: StandardMaterial3D
var _hit_mat: StandardMaterial3D
@@ -88,6 +100,18 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
body.mesh = parts.body
body.material_override = _mat
_visual.add_child(body)
if parts.has("eyes"):
# Eyes that catch the light: you see a wolf's eyes in the dark first.
var eyes := MeshInstance3D.new()
eyes.mesh = parts.eyes
var em := StandardMaterial3D.new()
em.albedo_color = Color(1.0, 0.85, 0.4)
em.emission_enabled = true
em.emission = Color(1.0, 0.75, 0.3)
em.emission_energy_multiplier = 2.5
eyes.material_override = em
eyes.set_meta("keep_mat", true)
_visual.add_child(eyes)
for h: Vector3 in parts.hips:
var pivot := Node3D.new()
pivot.position = h
@@ -105,7 +129,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
cs.position = Vector3(0, parts.height * 0.5, 0)
if k != "rabbit":
# Long bodies: lay the capsule along the spine.
cap.height = 1.5 if k == "deer" else 1.4
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25}.get(k, 1.4)
cs.rotation.x = PI * 0.5
cs.position = Vector3(0, parts.height * 0.55, 0)
add_child(cs)
@@ -219,12 +243,19 @@ func _move(dt: float) -> void:
yaw = rotate_toward(yaw, target_yaw, def.turn * dt)
if speed < 0.01:
return
_detour_t = maxf(_detour_t - dt, 0.0)
var dir := Vector3(-sin(yaw), 0, -cos(yaw))
if _blocked(dir):
# Turn away; fleeing animals turn harder.
var turn := rng.randf_range(0.8, 2.2) * (1.0 if rng.randf() < 0.5 else -1.0)
# Turn away and keep to the new heading for a moment (a detour), so
# an animal heading somewhere slides round an obstacle instead of
# turning straight back into it. Keep turning the same way while
# still blocked.
if _detour_t <= 0.0:
_detour_side = 1.0 if rng.randf() < 0.5 else -1.0
var turn := rng.randf_range(0.8, 1.6) * _detour_side
target_yaw = yaw + turn
yaw += turn * 0.35
_detour_t = rng.randf_range(1.0, 1.8)
speed *= 0.5
if state == "charge":
return
@@ -253,6 +284,13 @@ func _blocked(dir: Vector3) -> bool:
return not get_world_3d().direct_space_state.intersect_ray(q).is_empty()
func _animate(dt: float) -> void:
if state == "rest":
# Lying down: body lowered, legs folded under.
_visual.position.y = lerpf(_visual.position.y, -0.3, minf(dt * 3.0, 1.0))
_visual.rotation.x = lerpf(_visual.rotation.x, 0.0, minf(dt * 4.0, 1.0))
for l in _legs:
l.rotation.x = lerpf(l.rotation.x, 1.35 * (1.0 if l.position.z < 0.0 else -1.0), minf(dt * 3.0, 1.0))
return
var k := speed / maxf(def.run, 0.01)
_phase += dt * (3.0 + speed * 2.2)
var amp := clampf(speed / 2.0, 0.0, 1.0) * 0.6
@@ -288,6 +326,10 @@ func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
if hp <= 0.0:
_die(from)
return
_react(from)
## How a living animal responds to being hit (wolves override this).
func _react(from: Vector3) -> void:
if kind == "boar" and hp > def.hp * 0.3:
state = "charge"
_aggro = def.aggro
@@ -328,10 +370,10 @@ func butcher_drops() -> Dictionary:
func _set_material(m: Material) -> void:
for c in _visual.get_children():
if c is MeshInstance3D:
if c is MeshInstance3D and not c.has_meta("keep_mat"):
c.material_override = m
for g in c.get_children():
if g is MeshInstance3D:
if g is MeshInstance3D and not g.has_meta("keep_mat"):
g.material_override = m
func _burst() -> void: