M2.5: wildlife, wolves, hunting, fishing and sleeping on the server
This commit is contained in:
257
scripts/net/creature_net.gd
Normal file
257
scripts/net/creature_net.gd
Normal file
@@ -0,0 +1,257 @@
|
||||
extends Node
|
||||
## Autoload "CreatureNet": the wildlife on a server (M2.5,
|
||||
## docs/MULTIPLAYER_PLAN.md §5).
|
||||
##
|
||||
## The server's world runs the animals and the wolf pack (world.gd in server
|
||||
## mode), so everyone sees the same rabbit in the same place, and a pack
|
||||
## hunts whoever it likes. Ten times a second it sends everyone:
|
||||
## - every animal and wolf, alive or a carcass: id, kind, where, facing,
|
||||
## what it's doing, its speed and health;
|
||||
## - every fishing float: whose it is, where, and whether a fish is biting.
|
||||
## Players' games show these as puppets (creature.gd, bobber.gd `puppet`):
|
||||
## they animate and can be hit or butchered (Actions sends that to the
|
||||
## server), but decide nothing. Arrows and spears in flight are sent once
|
||||
## when they're loosed and flown on each screen for show; only the server's
|
||||
## copy hits anything. Howls are sent too.
|
||||
##
|
||||
## Snapshots are unreliable (a lost one is replaced 0.1 s later). Something
|
||||
## missing from the snapshots for a second (despawned, butchered) goes.
|
||||
|
||||
const Creature = preload("res://scripts/creatures/creature.gd")
|
||||
const Bobber = preload("res://scripts/fishing/bobber.gd")
|
||||
const Projectile = preload("res://scripts/creatures/projectile.gd")
|
||||
const WolfPack = preload("res://scripts/creatures/wolf_pack.gd")
|
||||
const SEND_INTERVAL := 0.1
|
||||
const FORGET_AFTER := 1.0
|
||||
|
||||
# Server.
|
||||
var _next_cid := 1
|
||||
var _by_cid: Dictionary = {} ## cid -> creature (server)
|
||||
var _send_left := 0.0
|
||||
|
||||
# Client.
|
||||
var world: Node3D
|
||||
var puppets: Dictionary = {} ## cid -> creature.gd puppet
|
||||
var floats: Dictionary = {} ## owner peer -> bobber.gd puppet
|
||||
var _seen: Dictionary = {} ## cid or "f<peer>" -> last time seen (s)
|
||||
var _root: Node3D
|
||||
var _howl_player: AudioStreamPlayer3D
|
||||
var projectiles_seen := 0 ## tests read these
|
||||
var howls_heard := 0
|
||||
|
||||
# --- server ----------------------------------------------------------------------
|
||||
|
||||
func _server_world() -> Node3D:
|
||||
return WorldNet.world if Net.is_server() and WorldNet.world != null and is_instance_valid(WorldNet.world) else null
|
||||
|
||||
## The server's creature with id `cid`, alive or a carcass.
|
||||
func creature(cid: int) -> Node3D:
|
||||
var c: Node3D = _by_cid.get(cid)
|
||||
return c if c != null and is_instance_valid(c) and not c.is_queued_for_deletion() else null
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var w := _server_world()
|
||||
if w != null:
|
||||
_server_tick(w, delta)
|
||||
elif world != null:
|
||||
_client_tick()
|
||||
|
||||
func _server_tick(w: Node3D, delta: float) -> void:
|
||||
if not w.launched.is_connected(_on_launched):
|
||||
w.launched.connect(_on_launched)
|
||||
_send_left -= delta
|
||||
if _send_left > 0.0:
|
||||
return
|
||||
_send_left = SEND_INTERVAL
|
||||
var all: Array = []
|
||||
_by_cid.clear()
|
||||
for group in ["creature", "carcass"]:
|
||||
for c in w.get_tree().get_nodes_in_group(group):
|
||||
if not c.has_meta("cid"):
|
||||
c.set_meta("cid", _next_cid)
|
||||
_next_cid += 1
|
||||
_by_cid[int(c.get_meta("cid"))] = c
|
||||
all.append(c)
|
||||
var fl: Array = []
|
||||
for p in w.bobbers:
|
||||
var b: Node3D = w.bobbers[p]
|
||||
if is_instance_valid(b) and is_instance_valid(p):
|
||||
fl.append([int(p.peer), b.global_position, b.state])
|
||||
if all.is_empty() and fl.is_empty():
|
||||
return
|
||||
# Packed tight and split, so no packet goes over the MTU (a lost
|
||||
# fragment loses the whole unreliable packet).
|
||||
var i := 0
|
||||
while i < maxi(all.size(), 1):
|
||||
var chunk := PackedByteArray()
|
||||
chunk.resize(mini(PER_PACKET, all.size() - i) * ENTRY)
|
||||
var o := 0
|
||||
for c in all.slice(i, i + PER_PACKET):
|
||||
_encode(chunk, o, c)
|
||||
o += ENTRY
|
||||
for peer in WorldNet._ready_peers:
|
||||
_s2c_life.rpc_id(peer, chunk, fl if i == 0 else [])
|
||||
i += PER_PACKET
|
||||
|
||||
const PER_PACKET := 40
|
||||
const ENTRY := 23
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf"]
|
||||
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend"]
|
||||
|
||||
## One creature in 23 bytes: id, kind, state, position, facing, speed, health.
|
||||
static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void:
|
||||
buf.encode_u32(o, int(c.get_meta("cid")))
|
||||
buf.encode_u8(o + 4, maxi(KIND_IDS.find(c.kind), 0))
|
||||
buf.encode_u8(o + 5, maxi(STATE_IDS.find(c.state), 0))
|
||||
var p: Vector3 = c.global_position
|
||||
buf.encode_float(o + 6, p.x)
|
||||
buf.encode_float(o + 10, p.y)
|
||||
buf.encode_float(o + 14, p.z)
|
||||
buf.encode_u16(o + 18, int(fposmod(c.yaw, TAU) / TAU * 65535.0))
|
||||
buf.encode_u8(o + 20, clampi(int(c.speed * 20.0), 0, 255))
|
||||
buf.encode_u16(o + 21, clampi(int(c.hp * 100.0), 0, 65535))
|
||||
|
||||
static func _decode(buf: PackedByteArray, o: int) -> Array:
|
||||
return [buf.decode_u32(o), KIND_IDS[mini(buf.decode_u8(o + 4), KIND_IDS.size() - 1)],
|
||||
Vector3(buf.decode_float(o + 6), buf.decode_float(o + 10), buf.decode_float(o + 14)),
|
||||
buf.decode_u16(o + 18) / 65535.0 * TAU, STATE_IDS[mini(buf.decode_u8(o + 5), STATE_IDS.size() - 1)],
|
||||
buf.decode_u8(o + 20) / 20.0, buf.decode_u16(o + 21) / 100.0]
|
||||
|
||||
func _on_launched(id: String, origin: Vector3, vel: Vector3, shooter: Node3D) -> void:
|
||||
var by := int(shooter.peer) if shooter != null and is_instance_valid(shooter) and shooter.get("peer") != null else 0
|
||||
for peer in WorldNet._ready_peers:
|
||||
_s2c_launched.rpc_id(peer, id, origin, vel, by)
|
||||
|
||||
## The pack howls (wolf_pack.gd): everyone hears it, from where it is.
|
||||
func howl(at: Vector3) -> void:
|
||||
for peer in WorldNet._ready_peers:
|
||||
_s2c_howl.rpc_id(peer, at)
|
||||
|
||||
# --- client ----------------------------------------------------------------------
|
||||
|
||||
func join_world(w: Node3D) -> void:
|
||||
world = w
|
||||
puppets.clear()
|
||||
floats.clear()
|
||||
_seen.clear()
|
||||
projectiles_seen = 0
|
||||
howls_heard = 0
|
||||
_root = Node3D.new()
|
||||
_root.name = "NetCreatures"
|
||||
w.add_child(_root)
|
||||
|
||||
func leave_world() -> void:
|
||||
if not Net.is_server():
|
||||
world = null
|
||||
puppets.clear()
|
||||
floats.clear()
|
||||
_root = null
|
||||
|
||||
static func _now() -> float:
|
||||
return Time.get_ticks_msec() / 1000.0
|
||||
|
||||
func _alive_world() -> bool:
|
||||
return world != null and is_instance_valid(world) and _root != null and is_instance_valid(_root)
|
||||
|
||||
## Anything the server has stopped sending goes.
|
||||
func _client_tick() -> void:
|
||||
if not _alive_world():
|
||||
return
|
||||
var now := _now()
|
||||
for cid in puppets.keys():
|
||||
if now - float(_seen.get(cid, 0.0)) > FORGET_AFTER:
|
||||
var c: Variant = puppets[cid]
|
||||
puppets.erase(cid)
|
||||
if is_instance_valid(c):
|
||||
(c as Node).queue_free()
|
||||
for peer in floats.keys():
|
||||
if now - float(_seen.get("f%d" % peer, 0.0)) > FORGET_AFTER * 0.6:
|
||||
_drop_float(peer)
|
||||
|
||||
func _drop_float(peer: int) -> void:
|
||||
var b: Variant = floats.get(peer) # (may already be freed: untyped)
|
||||
floats.erase(peer)
|
||||
if is_instance_valid(b):
|
||||
(b as Node).queue_free()
|
||||
if peer == Net.local_peer and world != null and is_instance_valid(world):
|
||||
world.bobber = null
|
||||
|
||||
@rpc("authority", "call_remote", "unreliable_ordered")
|
||||
func _s2c_life(packed: PackedByteArray, fl: Array) -> void:
|
||||
if not _alive_world():
|
||||
return
|
||||
var now := _now()
|
||||
var list: Array = []
|
||||
for o in range(0, packed.size() - ENTRY + 1, ENTRY):
|
||||
list.append(_decode(packed, o))
|
||||
for e in list:
|
||||
var cid := int(e[0])
|
||||
var kind := String(e[1])
|
||||
if not Creature.KINDS.has(kind):
|
||||
continue
|
||||
var got: Variant = puppets.get(cid)
|
||||
var c: Node3D = got if is_instance_valid(got) else null
|
||||
if c == null:
|
||||
c = Creature.new()
|
||||
c.puppet = true
|
||||
c.setup(kind, world, e[2], cid)
|
||||
c.set_meta("cid", cid)
|
||||
c.yaw = float(e[3])
|
||||
_root.add_child(c)
|
||||
c.global_position = e[2]
|
||||
c.rotation.y = c.yaw
|
||||
puppets[cid] = c
|
||||
c.net_update(e[2], float(e[3]), String(e[4]), float(e[5]), float(e[6]))
|
||||
_seen[cid] = now
|
||||
for f in fl:
|
||||
_float_update(int(f[0]), f[1], String(f[2]))
|
||||
_seen["f%d" % int(f[0])] = now
|
||||
|
||||
func _float_update(peer: int, pos: Vector3, state: String) -> void:
|
||||
var got: Variant = floats.get(peer)
|
||||
var b: Node3D = got if is_instance_valid(got) else null
|
||||
if b == null:
|
||||
b = Bobber.new()
|
||||
b.puppet = true
|
||||
b.setup(world, world.player, pos, Vector3.ZERO, 0)
|
||||
_root.add_child(b)
|
||||
floats[peer] = b
|
||||
if peer == Net.local_peer:
|
||||
world.bobber = b # hands.gd: the prompt, the line from our rod
|
||||
b.global_position = pos
|
||||
b.state = state
|
||||
if peer != Net.local_peer:
|
||||
# Someone else's line: from about where their rod would be.
|
||||
var av: Node3D = world.net_sync.avatars.get(peer) if world.net_sync != null else null
|
||||
if av != null and is_instance_valid(av):
|
||||
b.tip = av.global_position + Vector3(0, 1.5, 0) + (-av.global_basis.z) * 0.8
|
||||
else:
|
||||
b.tip = pos + Vector3(0, 0.1, 0)
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_launched(id: String, origin: Vector3, vel: Vector3, by: int) -> void:
|
||||
if not _alive_world():
|
||||
return
|
||||
projectiles_seen += 1
|
||||
var pr: Node3D = Projectile.new()
|
||||
pr.visual = true
|
||||
pr.setup(id, world, origin, vel, 0.0, world.player if by == Net.local_peer else null)
|
||||
_root.add_child(pr)
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_howl(at: Vector3) -> void:
|
||||
if not _alive_world():
|
||||
return
|
||||
howls_heard += 1
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return
|
||||
if _howl_player == null or not is_instance_valid(_howl_player):
|
||||
_howl_player = AudioStreamPlayer3D.new()
|
||||
_howl_player.stream = WolfPack.howl_stream()
|
||||
_howl_player.unit_size = 30.0
|
||||
_howl_player.max_distance = 400.0
|
||||
_howl_player.volume_db = 2.0
|
||||
_root.add_child(_howl_player)
|
||||
_howl_player.global_position = at
|
||||
_howl_player.pitch_scale = randf_range(0.9, 1.1)
|
||||
_howl_player.play()
|
||||
@@ -32,7 +32,7 @@ signal clock(day: int, time: float) ## the server's time of day
|
||||
const Identity = preload("res://scripts/net/identity.gd")
|
||||
const ServerConfig = preload("res://scripts/net/server_config.gd")
|
||||
|
||||
const PROTOCOL := 4 ## 3: the shared world (M2.3); 4: the server decides (M2.4)
|
||||
const PROTOCOL := 5 ## 3: shared world (M2.3); 4: the server decides (M2.4); 5: wildlife (M2.5)
|
||||
const DEFAULT_PORT := 24242
|
||||
const HANDSHAKE_TIMEOUT := 10.0
|
||||
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
||||
@@ -272,6 +272,12 @@ func _tick_clock(delta: float) -> void:
|
||||
for peer in players:
|
||||
_s2c_clock.rpc_id(peer, _world.day_night.day, _world.day_night.time)
|
||||
|
||||
## Everyone gets the clock now (after a night's sleep, an admin's /time).
|
||||
func push_clock() -> void:
|
||||
_clock_left = CLOCK_INTERVAL
|
||||
for peer in players:
|
||||
_s2c_clock.rpc_id(peer, _world.day_night.day, _world.day_night.time)
|
||||
|
||||
## Where a player is, as far as the server knows.
|
||||
func player_pos(peer: int) -> Vector3:
|
||||
return _states[peer].pos if _states.has(peer) else Vector3.INF
|
||||
@@ -440,6 +446,7 @@ func _c2s_proof(sig: PackedByteArray) -> void:
|
||||
if start == Vector3.INF:
|
||||
start = _spawn_for(players.size())
|
||||
PlayerNet.place_at(peer, start)
|
||||
_world.creatures.populate_for(start) # some wildlife about them straight away
|
||||
players[peer] = p
|
||||
_states[peer] = {"pos": start, "yaw": yaw, "pitch": 0.0, "anim": 0, "held": "", "t": server_time, "corr_t": -10.0,
|
||||
"stamp": server_time, "off": INF}
|
||||
|
||||
@@ -130,3 +130,4 @@ func _on_clock(day: int, time: float) -> void:
|
||||
if absf(diff) > 0.01:
|
||||
world.day_night.time = time
|
||||
world.day_night.apply(true)
|
||||
world.weather.snap() # (a night slept through: the weather jumps too)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
extends Node3D
|
||||
extends CharacterBody3D
|
||||
## One player's character on a dedicated server (M2.4): what they carry, what
|
||||
## they hold, what they wear, their bed, and their survival stats, which run
|
||||
## here (warmth from the server's fires and buildings, hunger, diet...).
|
||||
@@ -23,8 +23,12 @@ var worn := ""
|
||||
var cursor = null ## the stack held on the inventory screen's cursor
|
||||
var survival: Node
|
||||
var hands: Node ## (self: held_heat for a carried torch)
|
||||
var velocity := Vector3.ZERO
|
||||
## (A body with no collision, so it has a physics id: wolves look for a clear
|
||||
## line to it, and arrows skip whoever shot them. `velocity` comes from the
|
||||
## player's moves.)
|
||||
var water_level := -INF
|
||||
var in_bed := false ## got into bed, waiting for the night to pass
|
||||
var bed_at := Vector3.ZERO
|
||||
var has_bed_spawn := false
|
||||
var bed_spawn := Vector3.ZERO
|
||||
var anim := 0 ## last movement state reported (avatar.gd Anim)
|
||||
@@ -32,6 +36,9 @@ var died_at := 0 ## ticks (ms) when they last died
|
||||
|
||||
func setup(world: Node3D) -> void:
|
||||
hands = self
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
set_physics_process(false)
|
||||
water_level = world.terrain.water_level
|
||||
survival = preload("res://scripts/player/survival.gd").new()
|
||||
survival.name = "Survival"
|
||||
|
||||
Reference in New Issue
Block a user