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

@@ -19,7 +19,6 @@ signal cursor_changed(stack) ## client: the server says this is on your cur
const ServerPlayer = preload("res://scripts/net/server_player.gd")
const STATS_INTERVAL := 0.25
const NOT_YET := "Hunting and fishing aren't on servers yet (they come with the animals)"
# Server.
var proxies: Dictionary = {} ## peer -> server_player.gd
@@ -69,6 +68,10 @@ func server_join(peer: int, id: String, display: String) -> Vector3:
p.inventory.changed.connect(func(): _dirty[peer] = true; WorldNet.mark_changed())
p.worn_changed.connect(func(): _dirty[peer] = true)
p.survival.died.connect(_on_died.bind(peer))
# A wolf bite or a boar: their screen flashes red, from the right side.
p.survival.hurt.connect(func(amount, from):
if proxies.has(peer):
_s2c_hurt.rpc_id(peer, amount, from))
var at := Vector3.INF
var d := _read(_file(id))
if not d.is_empty():
@@ -96,10 +99,14 @@ func server_leave(peer: int) -> void:
if p.survival.dead:
_respawn(peer, false) # left on the death screen: they come back alive
_save(p)
var w: Node3D = WorldNet.world
if w != null and w.bobber_of(p) != null:
w.end_fishing(p)
proxies.erase(peer)
_dirty.erase(peer)
_acks.erase(peer)
p.queue_free()
_maybe_pass_night() # (the last one awake leaving lets the others sleep)
func save_all() -> void:
for p in proxies.values():
@@ -149,6 +156,9 @@ func server_moved(peer: int, from: Vector3, to: Vector3, yaw: float, anim: int,
p.rotation.y = yaw
if to.y < p.water_level - 0.6:
p.survival.soak()
if p.in_bed and to.distance_to(p.bed_at) > 1.5:
p.in_bed = false
tell(p.peer, "You got out of bed")
func _process(delta: float) -> void:
if not Net.is_server() or proxies.is_empty():
@@ -356,8 +366,129 @@ func _run(p: Node3D, what: String, a: Array) -> void:
"debug_drop":
if cheats and not ItemDB.get_item(_s(a, 0)).is_empty():
w.drop_item(_s(a, 0), maxi(_i(a, 1), 1), Transform3D(Basis(), _v(a, 2)))
# --- hunting and fishing (M2.5) ---
# Arrows, spears and casts start from where the server knows you are
# (your eyes), whatever your game says: only the aim comes from you.
"melee":
var c: Node3D = CreatureNet.creature(_i(a, 0))
if c != null:
Actions.melee(p, c)
"butcher":
var c: Node3D = CreatureNet.creature(_i(a, 0))
if c != null:
Actions.butcher(p, c)
"shoot":
var dir := _dir(a, 0)
var power := clampf(float(a[1]) if a.size() > 1 and (a[1] is float or a[1] is int) else 0.0, 0.0, 1.0)
if dir != Vector3.ZERO and Actions.can_draw(p):
Actions.shoot(p, _eye(p) + dir * 0.5, dir, power, w)
"throw":
var dir := _dir(a, 1)
if dir != Vector3.ZERO:
Actions.throw_spear(p, _i(a, 0), _eye(p) + dir * 0.5 + Vector3(0, -0.05, 0), dir + Vector3(0, 0.06, 0), w)
"cast":
var dir := _dir(a, 0)
if dir != Vector3.ZERO:
Actions.cast_line(p, _eye(p) + dir * 0.6, dir, w)
"reel":
Actions.reel(p, w)
"debug_spawn":
if cheats and Creature.KINDS.has(_s(a, 0)) and _s(a, 0) != "wolf" and _v(a, 1).is_finite():
w.creatures.spawn(_s(a, 0), _v(a, 1))
"debug_time":
if cheats:
w.day_night.day = maxi(_i(a, 0), 1)
w.day_night.time = clampf(float(a[1]) / 24.0, 0.0, 0.999) if a.size() > 1 else w.day_night.time
w.day_night.apply(true)
w.weather.snap()
Net.push_clock()
"debug_tp":
var to := _v(a, 0)
if cheats and to.is_finite():
p.global_position = to
Net.teleport(p.peer, to)
_s2c_respawn.rpc_id(p.peer, to, p.rotation.y, "")
"debug_hut":
if cheats and _v(a, 0).is_finite():
_debug_hut(_v(a, 0))
_:
Actions.notify.emit(NOT_YET if what in ["hunt", "fish"] else "Unknown request")
Actions.notify.emit("Unknown request")
const Creature = preload("res://scripts/creatures/creature.gd")
static func _eye(p: Node3D) -> Vector3:
return p.global_position + Vector3(0, 1.6, 0)
## A unit aim direction from the request, or ZERO.
static func _dir(a: Array, k: int) -> Vector3:
var d := _v(a, k)
return d.normalized() if d.is_finite() and d.length() > 0.01 else Vector3.ZERO
## Test hook: a closed hut (floors, walls, door, roof) with two beds in it.
func _debug_hut(center: Vector3) -> void:
var w: Node3D = WorldNet.world
var out: Dictionary = await w.build.debug_build_hut(center, 0.0)
if out.pieces.is_empty():
return
var top: float = out.pieces[0].global_position.y
for x in [-0.6, 0.6]:
w.spawn_piece(ItemDB.get_item("bed"), Transform3D(Basis(), Vector3(center.x + x, top, center.z - 0.3)))
# --- server: messages, getting hurt, sleeping ----------------------------------------
func tell(peer: int, text: String) -> void:
if proxies.has(peer):
_s2c_notify.rpc_id(peer, text)
func tell_all(text: String) -> void:
for peer in proxies:
_s2c_notify.rpc_id(peer, text)
## The server's sleep rule (server.json "sleep"): how many of the players
## online must be in bed for the night to pass.
func _beds_needed(n: int) -> int:
match String(Net.config.v("sleep")):
"anyone": return 1
"half": return ceili(n / 2.0)
return n # "all_online" (the default)
## Actions.use_bed decided `p` may sleep: they get into bed, and the night
## passes once enough players are in bed.
func go_to_bed(p: Node3D) -> void:
p.in_bed = true
p.bed_at = p.global_position
if not _maybe_pass_night():
var n: int = _alive().size()
var asleep: int = _alive().filter(func(q): return q.in_bed).size()
tell(p.peer, "You're in bed (%d of %d asleep). The night passes when %s." % [asleep, n,
"everyone's in bed" if _beds_needed(n) >= n else "%d are" % _beds_needed(n)])
for q in _alive():
if q != p:
tell(q.peer, "%s is in bed (%d of %d asleep)" % [p.display, asleep, n])
func _alive() -> Array:
return proxies.values().filter(func(q): return not q.survival.dead)
## Everyone in bed (by the rule)? Then until 06:00 for all: the clock, fires,
## regrowth and weather move on; sleepers wake rested.
func _maybe_pass_night() -> bool:
var w: Node3D = WorldNet.world
var live := _alive()
if w == null or live.is_empty():
return false
var asleep := live.filter(func(q): return q.in_bed)
if asleep.size() < _beds_needed(live.size()):
return false
var secs: float = fposmod(6.0 - w.day_night.hours(), 24.0) / 24.0 * w.day_night.day_length
w.skip_time(secs)
for q in live:
if q.in_bed:
q.survival.after_sleep(secs)
q.in_bed = false
_s2c_slept.rpc_id(q.peer, asleep.has(q))
Net.push_clock()
WorldNet.save()
return true
## The chest or grave `nid`, if this player may use it right now.
func _container(p: Node3D, nid: int) -> Node3D:
@@ -451,3 +582,22 @@ func _s2c_respawn(at: Vector3, yaw: float, text: String) -> void:
@rpc("authority", "call_remote", "reliable")
func _s2c_notify(text: String) -> void:
Actions.notify.emit(text)
var hurts := 0 ## client: how often the server said we were hurt (tests read it)
@rpc("authority", "call_remote", "reliable")
func _s2c_hurt(amount: float, from: Vector3) -> void:
hurts += 1
if world != null and is_instance_valid(world):
world.player.survival.hurt.emit(amount, from)
## The night passed (we were asleep, or up while the others slept).
@rpc("authority", "call_remote", "reliable")
func _s2c_slept(was_asleep: bool) -> void:
if world == null or not is_instance_valid(world):
return
var text := "You slept until morning" if was_asleep else "The night passed while the others slept"
if world.hud != null and was_asleep:
world.hud.fade(func(): Actions.notify.emit(text))
else:
Actions.notify.emit(text)