M2.5: wildlife, wolves, hunting, fishing and sleeping on the server

This commit is contained in:
Paul Bailey
2026-11-04 21:25:10 +00:00
parent 26be17b05f
commit a211fe4c63
20 changed files with 1258 additions and 88 deletions

View File

@@ -107,16 +107,19 @@ func step(dt: float) -> void:
if not night and alive_count() < pack_size(day):
fill_den(false)
_gap = maxf(_gap - dt, 0.0)
for t in _gaps.keys():
_gaps[t] = maxf(float(_gaps[t]) - dt, 0.0)
if not is_instance_valid(t):
_gaps.erase(t)
_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:
if m.global_position.distance_to(den_pos) > 6.0 and _far_from_everyone(m.global_position, 45.0) \
and _far_from_everyone(den_pos, 35.0):
m.global_position = den.slot_pos(m.slot)
m.wounded = false
m.state = "rest"
@@ -146,9 +149,9 @@ func _warn(day: int) -> void:
return
_warned[day] = true
if day == DEN_DAY:
Actions.notify.emit("Wolves are howling up in the hills. They'll come hunting soon.")
Actions.tell_all("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.")
Actions.tell_all("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
@@ -157,7 +160,7 @@ 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:
if not force and not _far_from_everyone(den_pos, 35.0):
return
members = alive()
var used := {}
@@ -181,44 +184,119 @@ func fill_den(force: bool) -> void:
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
func _far_from_everyone(pos: Vector3, r: float) -> bool:
for p in world.players():
if p.global_position.distance_to(pos) < r:
return false
return true
# --- who the pack goes after ----------------------------------------------------
# In single-player that's you. On a server the pack splits between the
# players it's hunting: wolf 1 takes the first, wolf 2 the second, and so on,
# preferring players away from a fire (you can't be got at by a fire, so
# they'd rather go for someone who isn't). Each target gets their own circle
# and their own one-wolf-at-a-time darts.
var _targets: Array = []
var _target_t := 0.0
## Who wolf `w` is after right now (null: nobody about).
func target_for(w: Node) -> Node3D:
if not world.server_mode:
return world.player
if mode != "hunt":
return world.nearest_player((w as Node3D).global_position)
_refresh_targets()
if _targets.is_empty():
return null
var i := alive().find(w)
return _targets[maxi(i, 0) % _targets.size()]
## Players worth hunting, nearest the den first: those away from a fire if
## there are any, otherwise everyone (they'll circle the fire).
func _refresh_targets() -> void:
var now := Time.get_ticks_msec() / 1000.0
if now - _target_t < 1.0 and _targets.all(func(p): return is_instance_valid(p) and not p.survival.dead):
return
_target_t = now
var everyone: Array = world.players()
var open := everyone.filter(func(p): return fire_near(p) == null)
_targets = open if not open.is_empty() else everyone
_targets.sort_custom(func(a, b): return a.global_position.distance_to(den_pos) < b.global_position.distance_to(den_pos))
func _p(p: Node3D) -> Node3D:
return p if p != null else world.player
## The lit campfire `p` is sheltering by, if any.
func fire_near(p: Node3D) -> Node3D:
var pp: Vector3 = _p(p).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
## (Single-player name, used by tests.)
func player_fire() -> Node3D:
return fire_near(world.player)
func _torch(p: Node3D = null) -> bool:
var who := _p(p)
return who.get("hands") != null and who.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
func may_close_in(p: Node3D = null) -> bool:
return fire_near(p) == null and not _p(p).survival.dead
## The wolves going after the same player as `w`.
func _hunting_with(w: Node) -> Array:
var t := target_for(w)
return alive().filter(func(m): return target_for(m) == t)
## 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
var t := _p(target_for(w))
var fire := fire_near(t)
var center: Vector3 = fire.global_position if fire != null else t.global_position
var r := FIRE_RING if fire != null else RING * (1.3 if _torch(t) else 1.0)
var group := _hunting_with(w)
var n := maxi(group.size(), 1)
var a := _circle_a + TAU * float(maxi(group.find(w), 0)) / 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:
## Darting in: one wolf at a time per player being hunted.
var _lungers: Dictionary = {} ## target -> wolf
var _gaps: Dictionary = {} ## target -> seconds until the next dart
## A wolf asks for its turn to dart in at `p`.
func request_lunge(w: Node, p: Node3D = null) -> bool:
var t := _p(p)
if not world.server_mode:
if _lunger != null or _gap > 0.0 or not may_close_in(t) or alive_count() < 2:
return false
else:
var cur: Node = _lungers.get(t)
if cur != null and is_instance_valid(cur) and not cur.is_dead() and cur.state == "lunge":
return false
if float(_gaps.get(t, 0.0)) > 0.0 or not may_close_in(t) or alive_count() < 2:
return false
if w.global_position.distance_to(t.global_position) > RING * 1.8:
return false
_lunger = w
_lungers[t] = 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)
if not world.server_mode:
_gap = ATTACK_GAP * (2.5 if _torch() else 1.0) * _rng.randf_range(0.8, 1.3)
return
var t: Node3D = target_for(w) if w != null and is_instance_valid(w) else null
for k in _lungers.keys():
if _lungers[k] == w:
_lungers.erase(k)
if t != null:
_gaps[t] = ATTACK_GAP * (2.5 if _torch(t) 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:
@@ -227,7 +305,12 @@ func alert(_from: Vector3) -> void:
func howl() -> void:
var live := alive()
if _audio == null or live.is_empty():
if live.is_empty():
return
if world.server_mode:
CreatureNet.howl((live[0] as Node3D).global_position + Vector3(0, 1, 0)) # everyone hears it
return
if _audio == null:
return
_audio.global_position = (live[0] as Node3D).global_position + Vector3(0, 1, 0)
_audio.pitch_scale = _rng.randf_range(0.9, 1.1)