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:
267
scripts/creatures/wolf_pack.gd
Normal file
267
scripts/creatures/wolf_pack.gd
Normal file
@@ -0,0 +1,267 @@
|
||||
extends Node3D
|
||||
## The wolf pack: when wolves exist, what they're doing, and who attacks.
|
||||
##
|
||||
## Schedule (a grace period so you can build first):
|
||||
## - Days 1-2: the den is empty (old bones only).
|
||||
## - Day 3: wolves move in. They rest in the den by day and howl at night,
|
||||
## but don't hunt yet. A toast warns you.
|
||||
## - Night 4 onward: the pack hunts. It's 2 wolves to start, 3 from day 6,
|
||||
## 4 from day 9. At dawn they go back to the den. Wolves you killed are
|
||||
## replaced at the den during the day, when you aren't watching.
|
||||
##
|
||||
## Night is 19:30 to 05:30, the same window the bed allows sleeping in. The
|
||||
## small hours belong to the previous day's night.
|
||||
##
|
||||
## While hunting the pack circles you and lets one wolf at a time dart in
|
||||
## (every few seconds, slower if you hold a torch). Near a lit campfire it
|
||||
## circles wide around the fire and never closes in.
|
||||
|
||||
const WolfScript = preload("res://scripts/creatures/wolf.gd")
|
||||
|
||||
const DEN_DAY := 3 ## wolves arrive in the den
|
||||
const HUNT_DAY := 4 ## first night they hunt
|
||||
const ALERT_RADIUS := 14.0 ## by day: closer than this to the den and they watch you
|
||||
const DEFEND_RADIUS := 8.0 ## ...and closer than this they attack
|
||||
const FIRE_SAFE := 8.0 ## within this of a lit campfire, wolves won't close in
|
||||
const RING := 10.0 ## circling radius around you
|
||||
const FIRE_RING := 14.0 ## circling radius around your fire
|
||||
const ATTACK_GAP := 4.5 ## seconds between darts (x2.5 with a torch)
|
||||
|
||||
var world: Node3D
|
||||
var den: Node3D
|
||||
var den_pos := Vector3.ZERO
|
||||
var den_yaw := 0.0
|
||||
var mode := "none" ## none | den | hunt
|
||||
var members: Array = []
|
||||
var enabled := true
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _lunger: Node = null
|
||||
var _gap := 2.0
|
||||
var _circle_a := 0.0
|
||||
var _warned := {} ## day -> true once that night's toast was shown
|
||||
var _howl_left := 0.0
|
||||
var _audio: AudioStreamPlayer3D
|
||||
static var _howl: AudioStreamWAV
|
||||
|
||||
func setup(w: Node3D, d: Node3D) -> void:
|
||||
world = w
|
||||
den = d
|
||||
den_pos = d.position
|
||||
den_yaw = d.facing_out_yaw()
|
||||
_rng.seed = w.terrain.noise_seed + 779
|
||||
if DisplayServer.get_name() != "headless":
|
||||
_audio = AudioStreamPlayer3D.new()
|
||||
_audio.stream = howl_stream()
|
||||
_audio.unit_size = 30.0
|
||||
_audio.max_distance = 400.0
|
||||
_audio.volume_db = 2.0
|
||||
add_child(_audio)
|
||||
|
||||
## The "day" a moment belongs to: the hours after midnight count as the
|
||||
## previous day's night.
|
||||
func effective_day() -> int:
|
||||
var h: float = world.day_night.hours()
|
||||
return world.day_night.day - (1 if h < 5.5 else 0)
|
||||
|
||||
func is_night() -> bool:
|
||||
var h: float = world.day_night.hours()
|
||||
return h >= 19.5 or h < 5.5
|
||||
|
||||
static func pack_size(day: int) -> int:
|
||||
if day < DEN_DAY:
|
||||
return 0
|
||||
if day < 6:
|
||||
return 2
|
||||
if day < 9:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
func alive() -> Array:
|
||||
var out := []
|
||||
for m in members:
|
||||
if is_instance_valid(m) and not m.is_dead():
|
||||
out.append(m)
|
||||
return out
|
||||
|
||||
func alive_count() -> int:
|
||||
return alive().size()
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if enabled:
|
||||
step(delta)
|
||||
|
||||
## Pack-level logic (the wolves run their own step()).
|
||||
func step(dt: float) -> void:
|
||||
var day := effective_day()
|
||||
var night := is_night()
|
||||
var want := "none"
|
||||
if day >= DEN_DAY:
|
||||
want = "hunt" if night and day >= HUNT_DAY else "den"
|
||||
if want != mode:
|
||||
_enter(want, day)
|
||||
if mode == "none":
|
||||
return
|
||||
if night:
|
||||
_warn(day)
|
||||
# Top up the den by day while you're not looking.
|
||||
if not night and alive_count() < pack_size(day):
|
||||
fill_den(false)
|
||||
_gap = maxf(_gap - dt, 0.0)
|
||||
_circle_a += dt * 0.25
|
||||
if _lunger != null and (not is_instance_valid(_lunger) or _lunger.is_dead() or _lunger.state != "lunge"):
|
||||
_lunger = null
|
||||
# At dawn wolves head home; once one is well away from you (out of sight),
|
||||
# it slips straight back rather than picking its way across the island.
|
||||
if mode == "den":
|
||||
var pp: Vector3 = world.player.global_position
|
||||
for m in alive():
|
||||
if m.global_position.distance_to(den_pos) > 6.0 and m.global_position.distance_to(pp) > 45.0 \
|
||||
and pp.distance_to(den_pos) > 35.0:
|
||||
m.global_position = den.slot_pos(m.slot)
|
||||
m.wounded = false
|
||||
m.state = "rest"
|
||||
if night and day >= DEN_DAY:
|
||||
_howl_left -= dt
|
||||
if _howl_left <= 0.0:
|
||||
_howl_left = _rng.randf_range(50.0, 110.0)
|
||||
howl()
|
||||
|
||||
func _enter(m: String, day: int) -> void:
|
||||
mode = m
|
||||
if m == "none":
|
||||
for w in members:
|
||||
if is_instance_valid(w):
|
||||
w.queue_free()
|
||||
members.clear()
|
||||
return
|
||||
if m == "den":
|
||||
for w in alive():
|
||||
w.wounded = false
|
||||
if members.is_empty():
|
||||
fill_den(true)
|
||||
|
||||
## Once per night: a toast on the first howling night and the first hunt.
|
||||
func _warn(day: int) -> void:
|
||||
if _warned.has(day):
|
||||
return
|
||||
_warned[day] = true
|
||||
if day == DEN_DAY:
|
||||
Actions.notify.emit("Wolves are howling up in the hills. They'll come hunting soon.")
|
||||
elif day == HUNT_DAY:
|
||||
Actions.notify.emit("The wolves are hunting tonight. Stay near a fire or behind closed walls.")
|
||||
_howl_left = _rng.randf_range(2.0, 6.0)
|
||||
|
||||
## Brings the pack up to size, wolves lying in the den. `force` ignores
|
||||
## whether you're watching.
|
||||
func fill_den(force: bool) -> void:
|
||||
var need: int = pack_size(effective_day()) - alive_count()
|
||||
if need <= 0:
|
||||
return
|
||||
if not force and world.player.global_position.distance_to(den_pos) < 35.0:
|
||||
return
|
||||
members = alive()
|
||||
var used := {}
|
||||
for m in members:
|
||||
used[m.slot] = true
|
||||
var i := 0
|
||||
while need > 0 and i < 4:
|
||||
if not used.has(i):
|
||||
var w: AnimatableBody3D = WolfScript.new()
|
||||
w.setup("wolf", world, den.slot_pos(i), _rng.randi())
|
||||
w.pack = self
|
||||
w.slot = i
|
||||
w.state = "rest"
|
||||
w.yaw = den_yaw + (i - 1.5) * 0.6
|
||||
w.rotation.y = w.yaw
|
||||
add_child(w)
|
||||
members.append(w)
|
||||
need -= 1
|
||||
i += 1
|
||||
|
||||
func home_slot(w: Node) -> Vector3:
|
||||
return den.slot_pos(w.slot)
|
||||
|
||||
## The lit campfire you're sheltering by, if any.
|
||||
func player_fire() -> Node3D:
|
||||
var pp: Vector3 = world.player.global_position
|
||||
for f in get_tree().get_nodes_in_group("station:campfire"):
|
||||
if f.station_active() and (f as Node3D).global_position.distance_to(pp) < FIRE_SAFE:
|
||||
return f
|
||||
return null
|
||||
|
||||
func _torch() -> bool:
|
||||
return world.player.hands != null and world.player.hands.held_heat() > 0.0
|
||||
|
||||
## Wolves only close in when you're away from a fire.
|
||||
func may_close_in() -> bool:
|
||||
return player_fire() == null and not world.player.survival.dead
|
||||
|
||||
## Where wolf `w` should be while circling.
|
||||
func circle_point(w: Node) -> Vector3:
|
||||
var fire := player_fire()
|
||||
var center: Vector3 = fire.global_position if fire != null else world.player.global_position
|
||||
var r := FIRE_RING if fire != null else RING * (1.3 if _torch() else 1.0)
|
||||
var live := alive()
|
||||
var n := maxi(live.size(), 1)
|
||||
var a := _circle_a + TAU * float(live.find(w)) / n
|
||||
return center + Vector3(cos(a), 0, sin(a)) * r
|
||||
|
||||
## A wolf asks for its turn to dart in.
|
||||
func request_lunge(w: Node) -> bool:
|
||||
if _lunger != null or _gap > 0.0 or not may_close_in() or alive_count() < 2:
|
||||
return false
|
||||
if w.global_position.distance_to(world.player.global_position) > RING * 1.8:
|
||||
return false
|
||||
_lunger = w
|
||||
return true
|
||||
|
||||
func lunge_done(w: Node) -> void:
|
||||
if _lunger == w:
|
||||
_lunger = null
|
||||
_gap = ATTACK_GAP * (2.5 if _torch() else 1.0) * _rng.randf_range(0.8, 1.3)
|
||||
|
||||
## A wolf was hit: the whole pack turns on you.
|
||||
func alert(_from: Vector3) -> void:
|
||||
for m in alive():
|
||||
m._aggro = maxf(m._aggro, 20.0)
|
||||
|
||||
func howl() -> void:
|
||||
var live := alive()
|
||||
if _audio == null or live.is_empty():
|
||||
return
|
||||
_audio.global_position = (live[0] as Node3D).global_position + Vector3(0, 1, 0)
|
||||
_audio.pitch_scale = _rng.randf_range(0.9, 1.1)
|
||||
_audio.play()
|
||||
|
||||
## A synthesised wolf howl: a rising then falling tone with vibrato and a
|
||||
## breathy edge, ~2.8 s. Built once; no audio files.
|
||||
static func howl_stream() -> AudioStreamWAV:
|
||||
if _howl != null:
|
||||
return _howl
|
||||
var rate := 22050
|
||||
var secs := 2.8
|
||||
var n := int(rate * secs)
|
||||
var data := PackedByteArray()
|
||||
data.resize(n * 2)
|
||||
var phase := 0.0
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 5
|
||||
var lp := 0.0
|
||||
for i in range(n):
|
||||
var t := float(i) / rate
|
||||
var u := t / secs
|
||||
# Pitch: swoop up, hold, sag at the end.
|
||||
var f := 330.0 + 260.0 * smoothstep(0.0, 0.25, u) - 140.0 * smoothstep(0.65, 1.0, u)
|
||||
f += sin(t * TAU * 5.5) * 6.0 * smoothstep(0.2, 0.4, u)
|
||||
phase += TAU * f / rate
|
||||
var env := smoothstep(0.0, 0.08, u) * (1.0 - smoothstep(0.75, 1.0, u))
|
||||
lp += 0.15 * ((rng.randf() * 2.0 - 1.0) - lp)
|
||||
var s := (sin(phase) * 0.7 + sin(phase * 2.0) * 0.18 + sin(phase * 3.0) * 0.06 + lp * 0.12) * env * 0.6
|
||||
data.encode_s16(i * 2, int(clampf(s, -1.0, 1.0) * 32000.0))
|
||||
var wav := AudioStreamWAV.new()
|
||||
wav.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
wav.mix_rate = rate
|
||||
wav.stereo = false
|
||||
wav.data = data
|
||||
_howl = wav
|
||||
return wav
|
||||
Reference in New Issue
Block a user