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

@@ -759,12 +759,17 @@ func _make_grass_tile(k: Vector2i) -> Node3D:
## half width]. Set by world.gd whenever floors are built or removed; only the
## grass tiles around old and new floors are rebuilt.
var _grass_cuts: Array = []
## Permanent cuts (the wolf den floor): [transform, half width x, half depth z].
var static_cuts: Array = []
func set_grass_cuts(xforms: Array, half: float) -> void:
var old := _grass_cuts
_grass_cuts = []
for xf: Transform3D in xforms:
_grass_cuts.append([xf.affine_inverse(), xf.origin, half])
_grass_cuts.append([xf.affine_inverse(), xf.origin, half, half])
for c in static_cuts:
var xf: Transform3D = c[0]
_grass_cuts.append([xf.affine_inverse(), xf.origin, float(c[1]), float(c[2])])
var touched := {}
for c in old + _grass_cuts:
var p: Vector3 = c[1]
@@ -783,7 +788,7 @@ func set_grass_cuts(xforms: Array, half: float) -> void:
static func _under_cut(cuts: Array, x: float, z: float) -> bool:
for c in cuts:
var l: Vector3 = (c[0] as Transform3D) * Vector3(x, (c[1] as Vector3).y, z)
if absf(l.x) < c[2] and absf(l.z) < c[2]:
if absf(l.x) < c[2] and absf(l.z) < c[3]:
return true
return false

View File

@@ -47,6 +47,9 @@ var weather_fx: Node3D ## weather_fx.gd: rain/snow particles, sound
var build: Node ## build_system.gd: snapping, support, shelter
var creatures: Node3D ## spawner.gd: wildlife around the player
var _dropped: Node3D ## arrows, spears and other dropped items
var den: Node3D ## den.gd: the wolves' cave at the foot of the rim
var bobber: Node3D ## bobber.gd: the cast fishing float, if any
var wolves: Node3D ## wolf_pack.gd (null if this seed has no den site)
var has_bed_spawn := false
var bed_spawn := Vector3.ZERO ## position of the bed you respawn at
var _loading := false ## applying a save: defer support recompute
@@ -193,6 +196,10 @@ func sleep_block_reason(bed: Node3D) -> String:
return "You can only sleep at night (after 19:30)"
if not build.sheltered_at(bed.global_position):
return "Your bed needs a roof and walls around it"
if wolves != null:
for w in wolves.alive():
if w.state in ["stalk", "lunge", "defend", "retreat"] and w.global_position.distance_to(bed.global_position) < 40.0:
return "You can't sleep with wolves prowling outside"
return ""
## Sleeps until 06:00: the clock, fires, regrowth and weather all move on.
@@ -227,6 +234,18 @@ func _on_terrain_ready() -> void:
_spawn_yaw = atan2(-to_lake.x, -to_lake.y)
player.water_level = terrain.water_level
props.place_on(terrain, player if not menu_mode else _menu_cam, _spawn)
# The wolf den: part of the scenery, so it's there from day 1 (empty).
den = preload("res://scripts/creatures/den.gd").new()
den.name = "WolfDen"
add_child(den)
if den.place(self, props._rock_mat):
props.static_cuts.append([den.global_transform, 2.2, 3.4])
_update_grass_cuts()
if DisplayServer.get_name() != "headless":
var fish := preload("res://scripts/fishing/lake_fish.gd").new()
fish.name = "LakeFish"
add_child(fish)
fish.setup(terrain)
print("[world] scenery placed in %d ms" % (Time.get_ticks_msec() - t0))
if menu_mode:
return
@@ -242,6 +261,11 @@ func _on_terrain_ready() -> void:
if not "--no-creatures" in OS.get_cmdline_user_args():
creatures.populate()
creatures.enabled = true
if den.found:
wolves = preload("res://scripts/creatures/wolf_pack.gd").new()
wolves.name = "Wolves"
add_child(wolves)
wolves.setup(self, den)
_maybe_capture()
func _process(delta: float) -> void:
@@ -474,6 +498,19 @@ func drop_item(id: String, count: int, xf: Transform3D) -> Node3D:
pk.global_transform = xf
return pk
## Casts a fishing float (Actions validated it). One at a time.
func start_fishing(p: Node3D, origin: Vector3, vel: Vector3) -> Node3D:
end_fishing()
bobber = preload("res://scripts/fishing/bobber.gd").new()
bobber.setup(self, p, origin, vel, randi())
add_child(bobber)
return bobber
func end_fishing() -> void:
if bobber != null and is_instance_valid(bobber):
bobber.queue_free()
bobber = null
## Fires an arrow or thrown spear (Actions validated it).
func launch(id: String, origin: Vector3, vel: Vector3, damage: float, shooter: Node3D) -> Node3D:
var pr: Node3D = preload("res://scripts/creatures/projectile.gd").new()
@@ -643,6 +680,103 @@ func _capture_survival(prefix: String, cam: Camera3D) -> void:
hud._menu.close()
await _capture_weather(prefix, cam)
await _capture_creatures(prefix, cam)
await _capture_wolves_fishing(prefix, cam)
## The den by day with the pack resting, wolves stalking at night, and
## fishing from the lake shore.
func _capture_wolves_fishing(prefix: String, cam: Camera3D) -> void:
if wolves == null:
return
for c in creatures.get_children():
c.queue_free()
wolves.enabled = false
day_night.day = 5
day_night.time = 10.0 / 24.0
day_night.apply(true)
wolves.step(0.1)
var out: Vector3 = (den.mouth_pos() - den.position).normalized()
var eye: Vector3 = den.position + Basis(Vector3.UP, 0.35) * out * 11.0
eye.y = terrain.sample_height(eye.x, eye.z) + 1.7
player.select(0)
_aim_camera(cam, eye, den.position + Vector3(0, 1.0, 0))
for w in wolves.alive():
w.set_physics_process(false)
for _i in range(30):
w._animate(0.1)
await get_tree().create_timer(1.5).timeout
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("%s_den.png" % prefix)
print("[world] saved %s_den.png" % prefix)
# Night: the pack circling you in the open.
var site: Vector3 = build.debug_find_site(_spawn + Vector3(-10, 0, 10), 6.0)
site.y = terrain.sample_height(site.x, site.z)
day_night.time = 22.5 / 24.0
day_night.apply(true)
var live: Array = wolves.alive()
for i in range(live.size()):
var a := -0.5 + i * 0.7
var p: Vector3 = site + Vector3(sin(a), 0, -cos(a)) * (7.0 + i * 1.5)
live[i].global_position = Vector3(p.x, terrain.sample_height(p.x, p.z), p.z)
live[i].state = "stalk"
var to: Vector3 = site - p
live[i].yaw = atan2(-to.x, -to.z)
live[i].rotation.y = live[i].yaw
live[i]._visual.position.y = 0.0
for l in live[i]._legs:
l.rotation.x = 0.0
player.inventory.set_slot(1, {"id": "torch", "count": 1})
player.select(1)
var e2: Vector3 = site + Vector3(0, 1.7, 0)
_aim_camera(cam, e2, site + Vector3(0, 0.6, -8.0))
await get_tree().create_timer(1.5).timeout
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("%s_wolves_night.png" % prefix)
print("[world] saved %s_wolves_night.png" % prefix)
for w in live:
w.global_position = den.slot_pos(w.slot)
# Fishing at the lake in the evening light, a fish on the line.
day_night.time = 18.2 / 24.0
day_night.apply(true)
var lake: Vector2 = terrain.lake_center
var x: float = lake.x
while terrain.sample_height(x, lake.y) < terrain.water_level + 0.4 and x < lake.x + 60.0:
x += 0.5
var shore := Vector3(x + 1.0, 0, lake.y)
shore.y = terrain.sample_height(shore.x, shore.z)
player.inventory.set_slot(2, {"id": "fishing_rod", "count": 1})
player.select(2)
var fe: Vector3 = shore + Vector3(0, 1.7, 0)
# Midday, looking at the nearest fish, one mid-jump.
var lf: Node = get_node_or_null("LakeFish")
if lf != null and not lf._fish.is_empty():
day_night.time = 12.5 / 24.0
day_night.apply(true)
var t_now := Time.get_ticks_msec() / 1000.0
var best := 0
for i in range(lf._fish.size()):
if lf._swim_pos(i, t_now).distance_to(fe) < lf._swim_pos(best, t_now).distance_to(fe):
best = i
_aim_camera(cam, fe, lf._swim_pos(best, t_now))
lf._jump_left = 0.4
await get_tree().create_timer(0.75).timeout
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("%s_lake_fish.png" % prefix)
print("[world] saved %s_lake_fish.png (fish %.0f m away)" % [prefix, lf._swim_pos(best, t_now).distance_to(fe)])
day_night.time = 18.2 / 24.0
day_night.apply(true)
_aim_camera(cam, fe, Vector3(lake.x + 6.0, terrain.water_level - 1.5, lake.y + 2.0))
await get_tree().create_timer(0.3).timeout
start_fishing(player, player.hands.rod_tip(), Vector3.ZERO)
bobber.set_physics_process(false)
bobber.global_position = Vector3(lake.x + 9.0, terrain.water_level, lake.y + 1.5)
bobber.state = "biting"
await get_tree().create_timer(1.5).timeout
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("%s_fishing.png" % prefix)
print("[world] saved %s_fishing.png" % prefix)
end_fishing()
## Wildlife views: a rabbit, a deer and a boar in a meadow, the bow drawn on
## a deer, and a carcass with an arrow in it.