M2.4: the server decides online (characters, inventory, stats, death, owned graves, per-player saves); death screen with Respawn after 3 s

This commit is contained in:
Paul Bailey
2026-11-04 19:18:31 +00:00
parent 8e8f10eb96
commit 35a656e996
16 changed files with 1124 additions and 477 deletions

View File

@@ -54,11 +54,23 @@ func _give(player: Node, id: String, n: int) -> int:
return got
## Playing on a server: changes to the island go to it (world_net.gd), and
## come back to everyone, us included. Our own inventory and stats stay here
## until M2.4.
## come back to everyone, us included.
func _online() -> bool:
return WorldNet.is_client()
## Online, the server decides (player_net.gd): every intent is sent there as
## a request, and the server runs this same function on our character there.
## Offline nothing changes: the game decides.
func _ask(what: String, args: Array = []) -> bool:
PlayerNet.ask(what, args)
return true
const NOT_ONLINE := "Hunting and fishing aren't on servers yet (they come with the animals)"
func _not_online() -> bool:
notify.emit(NOT_ONLINE)
return false
func _in_reach(player: Node3D, target: Node3D, extra: float = 0.0) -> bool:
return player.global_position.distance_to(target.global_position) <= REACH + 2.5 + extra
@@ -83,6 +95,8 @@ func can_craft(player: Node, recipe_id: String) -> bool:
return inv.has_all(r.inputs) and station_near(player, r.station)
func craft(player: Node, recipe_id: String) -> bool:
if _online():
return _ask("craft", [recipe_id])
var r: Dictionary = ItemDB.recipes.get(recipe_id, {})
if r.is_empty():
return false
@@ -109,13 +123,9 @@ func craft(player: Node, recipe_id: String) -> bool:
func pickup(player: Node3D, pickup_node: Node3D) -> bool:
if not is_instance_valid(pickup_node) or not _in_reach(player, pickup_node):
return false
var id: String = pickup_node.item_id
if _online():
if player.inventory.space_for(id) < 1:
notify.emit("Inventory full")
return false
WorldNet.take(pickup_node) # first come, first served: the server decides
return true
return _ask("pickup", [WorldNet.pickup_key(pickup_node)]) # first come, first served
var id: String = pickup_node.item_id
if _give(player, id, pickup_node.amount) <= 0:
return false
pickup_node.collected()
@@ -125,6 +135,8 @@ func pickup(player: Node3D, pickup_node: Node3D) -> bool:
func harvest_hit(player: Node3D, node: Node3D, hit_point: Vector3) -> bool:
if not is_instance_valid(node) or node.depleted:
return false
if _online():
return _ask("hit", [String(node.uid), hit_point])
if player.global_position.distance_to(hit_point) > REACH + 1.0:
return false
var held: Dictionary = ItemDB.get_item(player.held_item_id())
@@ -135,9 +147,6 @@ func harvest_hit(player: Node3D, node: Node3D, hit_point: Vector3) -> bool:
if not player.survival.use_stamina(6.0):
notify.emit("Too tired")
return false
if _online():
WorldNet.hit(node, float(tool.get("damage", 1.0)), hit_point)
return true
var drops: Dictionary = node.hit(float(tool.get("damage", 1.0)), hit_point, player.global_position)
for id in drops:
_give(player, id, int(drops[id]))
@@ -152,6 +161,8 @@ func can_place(player: Node3D, slot: int) -> bool:
func place(player: Node3D, slot: int, xf: Transform3D, world: Node3D) -> bool:
if not can_place(player, slot):
return false
if _online():
return _ask("place", [slot, xf])
if player.global_position.distance_to(xf.origin) > 8.0:
return false
var id: String = player.inventory.get_slot(slot).id
@@ -160,9 +171,6 @@ func place(player: Node3D, slot: int, xf: Transform3D, world: Node3D) -> bool:
notify.emit("Can't place that here")
return false
player.inventory.take_from(slot, 1)
if _online():
WorldNet.place(id, xf) # (given back if the server says no)
return true
world.spawn_piece(def, xf)
return true
@@ -171,6 +179,8 @@ func place(player: Node3D, slot: int, xf: Transform3D, world: Node3D) -> bool:
func dismantle(player: Node3D, piece: Node3D) -> bool:
if not is_instance_valid(piece) or not piece.is_in_group("piece") or piece.is_queued_for_deletion():
return false
if _online():
return _ask("dismantle", [WorldNet.nid(piece)])
if player.global_position.distance_to(piece.global_position) > 8.0:
return false
if piece.item_id == "grave":
@@ -183,9 +193,6 @@ func dismantle(player: Node3D, piece: Node3D) -> bool:
if player.inventory.space_for(piece.item_id) < 1:
notify.emit("Inventory full")
return false
if _online():
WorldNet.dismantle(piece)
return true
_give(player, piece.item_id, 1)
piece.get_parent().get_parent().remove_piece(piece, player)
return true
@@ -194,8 +201,7 @@ func toggle_door(player: Node3D, door: Node3D) -> bool:
if not is_instance_valid(door) or not _in_reach(player, door, 1.0):
return false
if _online():
WorldNet.door(door)
return true
return _ask("door", [WorldNet.nid(door)])
door.set_open(not door.open)
return true
@@ -203,9 +209,16 @@ func toggle_door(player: Node3D, door: Node3D) -> bool:
func use_bed(player: Node3D, bed: Node3D) -> bool:
if not is_instance_valid(bed) or not _in_reach(player, bed, 1.0):
return false
if _online():
return _ask("bed", [WorldNet.nid(bed)])
var world: Node = bed.get_parent().get_parent()
if not bed.is_spawn():
world.set_bed_spawn(bed)
# On a server each player has their own bed (server_player.gd).
var mine: bool = player.claims_bed(bed) if player.has_method("claims_bed") else bed.is_spawn()
if not mine:
if player.has_method("claim_bed"):
player.claim_bed(bed)
else:
world.set_bed_spawn(bed)
notify.emit("Spawn point set")
return true
var why: String = world.sleep_block_reason(bed)
@@ -237,6 +250,8 @@ func melee_stats(player: Node) -> Dictionary:
## One melee blow at an animal.
func melee(player: Node3D, target: Node3D) -> bool:
if _online():
return _not_online()
if not is_instance_valid(target) or not target.is_in_group("creature"):
return false
var m := melee_stats(player)
@@ -264,6 +279,8 @@ func projectile_hit(shooter: Node3D, target: Node3D, damage: float, at: Vector3,
## Throws the spear in `slot` from `origin` along `dir`.
func throw_spear(player: Node3D, slot: int, origin: Vector3, dir: Vector3, world: Node3D) -> bool:
if _online():
return _not_online()
var s = player.inventory.get_slot(slot)
if s == null:
return false
@@ -281,6 +298,8 @@ func throw_spear(player: Node3D, slot: int, origin: Vector3, dir: Vector3, world
## Starts drawing the held bow: needs arrows and a little stamina.
func can_draw(player: Node3D) -> bool:
if _online():
return _not_online()
var bow: Dictionary = ItemDB.get_item(player.held_item_id()).get("bow", {})
if bow.is_empty():
return false
@@ -294,6 +313,8 @@ func can_draw(player: Node3D) -> bool:
## Looses an arrow. `power` 0..1 is how far the bow was drawn.
func shoot(player: Node3D, origin: Vector3, dir: Vector3, power: float, world: Node3D) -> bool:
if _online():
return _not_online()
var bow: Dictionary = ItemDB.get_item(player.held_item_id()).get("bow", {})
if bow.is_empty() or player.global_position.distance_to(origin) > 2.5:
return false
@@ -306,6 +327,8 @@ func shoot(player: Node3D, origin: Vector3, dir: Vector3, power: float, world: N
return true
func butcher(player: Node3D, carcass: Node3D) -> bool:
if _online():
return _not_online()
if not is_instance_valid(carcass) or not carcass.is_in_group("carcass") or not _in_reach(player, carcass, 1.0):
return false
var drops: Dictionary = carcass.butcher_drops()
@@ -322,6 +345,8 @@ func butcher(player: Node3D, carcass: Node3D) -> bool:
## Casts the held rod from `origin` along `dir`.
func cast_line(player: Node3D, origin: Vector3, dir: Vector3, world: Node3D) -> bool:
if _online():
return _not_online()
var rod: Dictionary = ItemDB.get_item(player.held_item_id()).get("fishing", {})
if rod.is_empty() or player.global_position.distance_to(origin) > 2.5:
return false
@@ -335,6 +360,8 @@ func cast_line(player: Node3D, origin: Vector3, dir: Vector3, world: Node3D) ->
## Reels in. With a fish biting you land it; otherwise the line just comes in.
func reel(player: Node3D, world: Node3D) -> bool:
if _online():
return _not_online()
var b: Node3D = world.bobber
if b == null or not is_instance_valid(b):
return false
@@ -351,6 +378,8 @@ func reel(player: Node3D, world: Node3D) -> bool:
## Puts on the cloak in `slot` (swapping out any cloak you were wearing).
func wear(player: Node3D, slot: int) -> bool:
if _online():
return _ask("wear", [slot])
var s = player.inventory.get_slot(slot)
if s == null or not ItemDB.get_item(s.id).has("wear"):
return false
@@ -366,6 +395,8 @@ func wear(player: Node3D, slot: int) -> bool:
func take_off(player: Node3D) -> bool:
if player.worn.is_empty():
return false
if _online():
return _ask("take_off")
if player.inventory.space_for(player.worn) < 1:
notify.emit("Inventory full")
return false
@@ -378,8 +409,7 @@ func forage(player: Node3D, bush: Node3D) -> bool:
if not is_instance_valid(bush) or not _in_reach(player, bush) or not bush.ripe():
return false
if _online():
WorldNet.forage(bush)
return true
return _ask("forage", [String(bush.uid)])
var got: Dictionary = bush.harvest()
for id in got:
_give(player, id, int(got[id]))
@@ -387,6 +417,8 @@ func forage(player: Node3D, bush: Node3D) -> bool:
## Eats one of the food in `slot`.
func eat(player: Node3D, slot: int) -> bool:
if _online():
return _ask("eat", [slot])
var s = player.inventory.get_slot(slot)
if s == null:
return false
@@ -406,16 +438,15 @@ func eat(player: Node3D, slot: int) -> bool:
func add_fuel(player: Node3D, fire: Node3D) -> bool:
if not is_instance_valid(fire) or not _in_reach(player, fire):
return false
if _online():
return _ask("fuel", [WorldNet.nid(fire)])
if fire.fuel >= fire.MAX_FUEL - 1.0:
notify.emit("The fire is fully stoked")
return false
for id in ["wood", "stick", "fiber"]:
if player.inventory.count(id) > 0:
player.inventory.remove(id, 1)
if _online():
WorldNet.fuel(fire, id)
else:
fire.add_fuel(float(ItemDB.get_item(id).get("fuel", 10)))
fire.add_fuel(float(ItemDB.get_item(id).get("fuel", 10)))
notify.emit("Added %s to the fire" % ItemDB.item_name(id).to_lower())
return true
notify.emit("Nothing to burn (wood or sticks)")
@@ -426,14 +457,8 @@ func add_fuel(player: Node3D, fire: Node3D) -> bool:
func transfer(player: Node3D, from: Inventory, slot: int, to: Inventory, container: Node3D = null) -> void:
if container != null and not _in_reach(player, container, 1.0):
return
if _online() and container != null:
var s = from.get_slot(slot)
if s == null:
return
if from == player.inventory:
from.set_slot(slot, null)
WorldNet.chest_put(container, s) # what doesn't fit comes back
else:
WorldNet.chest_take(container, slot, to.space_for(s.id))
if _online():
if container != null:
_ask("transfer", [WorldNet.nid(container), slot, from == player.inventory])
return
from.quick_move(slot, to)