Uses for every material (clay jar, bedroll, pelt rug, fang necklace, bear cloak, pemmican, bait), Q to drop, K/M bag rows on 1-8, help pages; protocol 14

This commit is contained in:
Paul Bailey
2026-11-06 11:49:42 +00:00
parent 5778d61763
commit 4b2e687188
29 changed files with 1457 additions and 40 deletions

View File

@@ -2,8 +2,15 @@ extends "res://scripts/building/piece.gd"
## Bed: E sets your spawn point here. Once it's yours, E at night sleeps until
## morning, if the bed is under a roof with walls around it.
func _build() -> void:
add_to_group("bed") # (a bed or a bedroll: either is a spawn point)
func display_name() -> String:
return "Bed"
return String(def.get("name", "Bed"))
## A bedroll: you can sleep in the open (world.sleep_block_reason).
func open_air() -> bool:
return bool(def.get("place", {}).get("open_air", false))
func is_spawn() -> bool:
var w := get_parent().get_parent()
@@ -11,8 +18,8 @@ func is_spawn() -> bool:
func interact_prompt(_player: Node) -> String:
if not is_spawn():
return "Bed [E] Set spawn point"
return "Bed (your spawn point) [E] Sleep"
return "%s [E] Set spawn point" % display_name()
return "%s (your spawn point) [E] Sleep" % display_name()
func interact(player: Node) -> void:
Actions.use_bed(player, self)

29
scripts/building/jar.gd Normal file
View File

@@ -0,0 +1,29 @@
extends "res://scripts/building/piece.gd"
## Clay jar: 6 slots of storage, opened with E like a chest, and food kept in
## it lasts three times as long (Inventory.keep_mult).
const SLOTS := 6
const KEEP := 3.0
var inventory := Inventory.new(SLOTS)
func _build() -> void:
inventory.keep_mult = KEEP
func display_name() -> String:
return "Clay Jar"
func dismantle_block_reason() -> String:
for s in inventory.slots:
if s != null:
return "Empty the jar first"
return ""
func interact_prompt(_player: Node) -> String:
var used := 0
for s in inventory.slots:
if s != null:
used += 1
return "Clay jar (%d/%d, food keeps 3x as long) [E] Open" % [used, SLOTS]
func interact(player: Node) -> void:
player.open_inventory(self)

View File

@@ -31,6 +31,9 @@ func _ready() -> void:
_bind("inventory", [KEY_TAB, KEY_I])
_bind("rotate_piece", [KEY_R])
_bind("dismantle", [KEY_X])
_bind("drop", [KEY_Q]) # (shift+Q drops just one)
_bind("row_up", [KEY_K]) # the 1-8 keys pick from the bag row above...
_bind("row_down", [KEY_M]) # ...or below
for i in range(8):
_bind("hotbar_%d" % (i + 1), [KEY_1 + i])
_bind_mouse("attack", MOUSE_BUTTON_LEFT)
@@ -736,8 +739,53 @@ static func harvest_sound(tool_kind: String, felled: bool) -> String:
## A piece going down: wood, stone, or something soft.
static func place_sound(id: String) -> String:
if id.contains("stone") or id in ["campfire", "forge", "hearth", "stonecutter"]:
if id.contains("stone") or id.contains("jar") or id in ["campfire", "forge", "hearth", "stonecutter"]:
return "place_stone"
if id.contains("rug") or id.contains("bed") or id == "smoking_rack":
return "place_soft"
return "place_wood"
# --- charms and bait ------------------------------------------------------------------
## A charm carried anywhere in the bag (the fang necklace: {"against": "wolf",
## "damage": 0.7}) takes the edge off hits from what it's against. `by` is the
## hit's "a wolf" / "a alpha wolf" / "a spirit wolf" (survival.damage).
static func charm_mult(inv: Inventory, by: String) -> float:
var m := 1.0
if by.is_empty():
return m
for s in inv.slots:
if s == null:
continue
var c: Dictionary = ItemDB.get_item(String(s.id)).get("charm", {})
if not c.is_empty() and by.to_lower().contains(String(c.get("against", "?"))):
m = minf(m, float(c.get("damage", 1.0)))
return m
const BAIT := "rotten_food"
const BAIT_WAIT := 0.6 ## carried while fishing: bites come this much sooner
## Drops the stack in `slot` (or `count` of it) at your feet, a little in
## front (Q drops what's in your hand). Online the server does it, for everyone.
func drop(player: Node3D, slot: int, count: int = 0) -> bool:
if _online():
return _ask("drop", [slot, count])
var s = player.inventory.get_slot(slot)
if s == null:
return false
var n: int = int(s.count) if count <= 0 else mini(count, int(s.count))
# (on a server the player is a proxy under PlayerNet, not a child of the world)
var world: Node = WorldNet.world if Net.is_server() else player.get_parent()
if world == null or not world.has_method("drop_item"):
return false
var fwd: Vector3 = -player.global_basis.z
fwd.y = 0.0
var at: Vector3 = player.global_position + fwd.normalized() * 0.9
at.y = world.terrain.sample_height(at.x, at.z) + 0.05 if world.get("terrain") != null else player.global_position.y
var extra := Inventory.extra_of(s)
if world.drop_item(String(s.id), n, Transform3D(Basis(), at), extra) == null:
return false
player.inventory.take_from(slot, n)
Sfx.at("pickup", at)
notify.emit("Dropped %d %s" % [n, ItemDB.item_name(String(s.id))])
return true

View File

@@ -65,6 +65,10 @@ static func extra_of(stack: Dictionary) -> Dictionary:
static var clock := 0.0
static var day_seconds := 1200.0
static var rot_speed := 1.0
## How many times longer food keeps in here (a clay jar: 3), and the clock at
## the last spoil() (the time since is what's slowed).
var keep_mult := 1.0
var _last_clock := -1.0
const ROTTEN := "rotten_food"
static func _db() -> Node:
@@ -110,6 +114,15 @@ func spoil() -> int:
return 0
var rotted := 0
var touched := false
# A clay jar (keep_mult 3): food in it ages at a third of the rate, so its
# "rot" stamp moves back by two thirds of the time that's passed.
if keep_mult > 1.0:
if _last_clock >= 0.0 and clock > _last_clock:
var later := (clock - _last_clock) * (1.0 - 1.0 / keep_mult)
for s in slots:
if s != null and s.has("rot") and float(s.rot) > _last_clock:
s.rot = float(s.rot) + later
_last_clock = clock
for i in range(slots.size()):
var s = slots[i]
if s == null or keeps_days(String(s.id)) <= 0.0:

View File

@@ -74,6 +74,8 @@ func _dawn_dusk() -> bool:
return (h > 5.0 and h < 8.5) or (h > 17.0 and h < 20.5)
## How long until the next bite (seconds), from data/fish.json.
var baited := false ## this wait was shortened by bait (rotten food)
func _next_wait() -> float:
var mods: Dictionary = ItemDB.fishing.modifiers
var w: Array = table.get("bite_wait", [5.0, 14.0])
@@ -82,6 +84,10 @@ func _next_wait() -> float:
k *= float(mods.get("dawn_dusk_wait", 0.6)) # fish feed at dawn and dusk
if world.weather.precip > 0.3:
k *= float(mods.get("rain_wait", 0.8))
# Rotten food in your bag is bait: the fish come sooner, and a bite takes one.
baited = player != null and player.get("inventory") != null and player.inventory.count(Actions.BAIT) > 0
if baited:
k *= Actions.BAIT_WAIT
return rng.randf_range(w[0], w[1]) * k
## A weighted pick from this water's fish (dawn/dusk weights when they apply).
@@ -139,6 +145,8 @@ func step(dt: float) -> void:
bite_left = BITE_WINDOW
Actions.tell(player, "Bite! Click to reel in")
Sfx.at("bite", global_position)
if baited and player.inventory.remove(Actions.BAIT, 1):
baited = false
"biting":
bite_left -= dt
# The float bobs hard and goes under.

View File

@@ -115,6 +115,12 @@ static func build(key: String) -> ArrayMesh:
"bear_hide": m = _pelt(BEAR_FUR)
"bear_fat": m = _pebble(271, Color(0.92, 0.86, 0.68), Vector3(0.1, 0.06, 0.08), 0.1)
"fur_rug": m = _rug()
"pelt_rug": m = _rug(WOLF_FUR, 0.85)
"bear_cloak": m = _cloak(BEAR_FUR)
"clay_jar": m = _jar()
"bedroll": m = _bedroll()
"fang_necklace": m = _necklace()
"pemmican": m = _pebble(331, Color(0.42, 0.24, 0.16), Vector3(0.1, 0.05, 0.07), 0.15)
"barrow_key": m = _barrow_key()
"rotten_food": m = _pebble(321, Color(0.33, 0.3, 0.16), Vector3(0.11, 0.06, 0.1), 0.45)
"salt": m = _salt()
@@ -999,19 +1005,58 @@ static func _fang() -> ArrayMesh:
const BEAR_FUR := Color(0.36, 0.24, 0.15)
## A bearskin laid flat: body, four splayed legs and the head toward -Z.
static func _rug() -> ArrayMesh:
## A skin rug with its head at -Z: the bear's (fur_rug), or wolves' a bit smaller.
static func _rug(fur: Color = BEAR_FUR, k: float = 1.0) -> ArrayMesh:
var st := P._begin()
var r := _rng(273)
_box(st, Vector3(0, 0.015, 0.1), Vector3(1.0, 0.03, 1.5), BEAR_FUR)
_box(st, Vector3(0, 0.02, 0.1), Vector3(0.8, 0.03, 1.3), P._mul(BEAR_FUR, 1.12))
_box(st, Vector3(0, 0.015, 0.1) * k, Vector3(1.0, 0.03 / k, 1.5) * k, fur)
_box(st, Vector3(0, 0.02, 0.1) * k, Vector3(0.8, 0.03 / k, 1.3) * k, P._mul(fur, 1.12))
for sx in [-1.0, 1.0]:
for sz in [-0.45, 0.6]:
_box(st, Vector3(sx * 0.62, 0.012, sz), Vector3(0.42, 0.024, 0.24), _board(r, BEAR_FUR), Basis(Vector3.UP, sx * (0.5 if sz < 0 else -0.5)))
_box(st, Vector3(0, 0.05, -0.85), Vector3(0.38, 0.1, 0.34), P._mul(BEAR_FUR, 0.95))
_box(st, Vector3(0, 0.045, -1.06), Vector3(0.18, 0.08, 0.14), Color(0.58, 0.45, 0.32))
_box(st, Vector3(sx * 0.62, 0.012, sz) * k, Vector3(0.42, 0.024 / k, 0.24) * k, _board(r, fur), Basis(Vector3.UP, sx * (0.5 if sz < 0 else -0.5)))
_box(st, Vector3(0, 0.05, -0.85) * k, Vector3(0.38, 0.1, 0.34) * k, P._mul(fur, 0.95))
_box(st, Vector3(0, 0.045, -1.06) * k, Vector3(0.18, 0.08, 0.14) * k, Color(0.58, 0.45, 0.32))
for sx in [-0.14, 0.14]:
_box(st, Vector3(sx, 0.11, -0.8), Vector3(0.08, 0.05, 0.05), P._mul(BEAR_FUR, 0.8))
_box(st, Vector3(0, 0.02, 0.92), Vector3(0.12, 0.03, 0.12), BEAR_FUR)
_box(st, Vector3(sx, 0.11, -0.8) * k, Vector3(0.08, 0.05, 0.05) * k, P._mul(fur, 0.8))
_box(st, Vector3(0, 0.02, 0.92) * k, Vector3(0.12, 0.03, 0.12) * k, fur)
return P._commit(st)
## A fired clay jar with a wooden lid, about knee high.
static func _jar() -> ArrayMesh:
var st := P._begin()
var r := _rng(337)
P._cylinder(st, Vector3(0, 0.0, 0), Vector3(0, 0.12, 0), 0.16, 0.26, 10, P._mul(CLAY_FIRED, 0.92), r, -1.0, CLAY_FIRED)
P._cylinder(st, Vector3(0, 0.12, 0), Vector3(0, 0.42, 0), 0.26, 0.24, 10, CLAY_FIRED, r)
P._cylinder(st, Vector3(0, 0.42, 0), Vector3(0, 0.52, 0), 0.24, 0.16, 10, P._mul(CLAY_FIRED, 1.06), r)
P._cylinder(st, Vector3(0, 0.52, 0), Vector3(0, 0.56, 0), 0.17, 0.17, 10, P._mul(CLAY_FIRED, 0.85), r)
P._cylinder(st, Vector3(0, 0.56, 0), Vector3(0, 0.6, 0), 0.19, 0.17, 10, PLANK_DARK, r, -1.0, PLANK) # lid
_box(st, Vector3(0, 0.63, 0), Vector3(0.08, 0.04, 0.05), PLANK_DARK)
return P._commit(st)
## A hide bedroll: a flat mat with its rolled-up end and a feather pillow.
static func _bedroll() -> ArrayMesh:
var st := P._begin()
var r := _rng(347)
_box(st, Vector3(0, 0.03, 0.15), Vector3(0.8, 0.06, 1.6), HIDE)
_box(st, Vector3(0, 0.065, 0.2), Vector3(0.66, 0.02, 1.35), P._mul(HIDE, 1.12))
P._cylinder(st, Vector3(-0.4, 0.13, 0.95), Vector3(0.4, 0.13, 0.95), 0.12, 0.12, 8, _board(r, HIDE), r, -1.0, P._mul(HIDE, 0.8))
_box(st, Vector3(0, 0.1, -0.5), Vector3(0.5, 0.09, 0.28), FEATHER)
for sx in [-0.25, 0.25]:
_box(st, Vector3(sx, 0.18, 0.95), Vector3(0.04, 0.26, 0.04), BINDING)
return P._commit(st)
## A cord with five fangs hanging from a dark scrap of pelt.
static func _necklace() -> ArrayMesh:
var st := P._begin()
var r := _rng(353)
for i in range(12):
var a := TAU * i / 12.0
var b := TAU * (i + 1) / 12.0
P._cylinder(st, Vector3(cos(a) * 0.12, 0.01, sin(a) * 0.1), Vector3(cos(b) * 0.12, 0.01, sin(b) * 0.1), 0.006, 0.006, 4, BINDING, r)
_box(st, Vector3(0, 0.015, 0.1), Vector3(0.09, 0.02, 0.05), ALPHA_FUR)
for i in range(5):
var x := (i - 2) * 0.035
P._cylinder(st, Vector3(x, 0.012, 0.12), Vector3(x * 1.2, 0.012, 0.19 - absf(i - 2) * 0.012), 0.012, 0.002, 5, Color(0.88, 0.82, 0.62), r)
return P._commit(st)
# --- P3 the barrow --------------------------------------------------------------------

View File

@@ -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 := 13 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3); 11: food spoilage, lightning (P4); 12: the standing stones and the Old Stag (P5); 13: sounds sent to players nearby (sfx.gd)
const PROTOCOL := 14 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3); 11: food spoilage, lightning (P4); 12: the standing stones and the Old Stag (P5); 13: sounds sent to players nearby (sfx.gd); 14: dropping items, any bag row in hand, new items (jar, bedroll, necklace, bear cloak, rug, pemmican)
const DEFAULT_PORT := 24242
const HANDSHAKE_TIMEOUT := 10.0
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join

View File

@@ -341,7 +341,7 @@ func _run(p: Node3D, what: String, a: Array) -> void:
WorldNet.piece_state(f)
"bed":
var b := WorldNet.piece(_i(a, 0))
if b != null and b.item_id == "bed" and _allowed(Rules.use_problem(p.player_id, b)):
if b != null and b.is_in_group("bed") and _allowed(Rules.use_problem(p.player_id, b)):
Actions.use_bed(p, b)
"inv_click":
# Slot `a[1]` of your own inventory (a[0] = 0) or a chest's.
@@ -365,6 +365,10 @@ func _run(p: Node3D, what: String, a: Array) -> void:
var slot := _i(a, 1)
if slot >= 0 and slot < from.size():
Actions.transfer(p, from, slot, to, c)
"drop":
var ds := _i(a, 0)
if ds >= 0 and ds < p.inventory.size():
Actions.drop(p, ds, _i(a, 1))
"close":
# The inventory screen closed: what's on the cursor goes back in
# your bag, or at your feet if it's full.

View File

@@ -63,7 +63,7 @@ func held_item_id() -> String:
return s.id if s != null else ""
func select(i: int) -> void:
selected = wrapi(i, 0, HOTBAR)
selected = wrapi(i, 0, inventory.size()) # (any bag slot: player.gd's rows)
selected_changed.emit()
func held_heat() -> float:
@@ -85,7 +85,7 @@ func bed_piece(world: Node3D) -> Node3D:
if not has_bed_spawn:
return null
for p in world.all_pieces():
if p.item_id == "bed" and claims_bed(p):
if p.is_in_group("bed") and claims_bed(p):
return p
return null

View File

@@ -237,13 +237,22 @@ func _unhandled_input(event: InputEvent) -> void:
return
for i in range(8):
if event.is_action_pressed("hotbar_%d" % (i + 1)):
player.select(i)
player.select_in_row(i)
return
if event.is_action_pressed("row_up"):
player.change_row(-1)
return
if event.is_action_pressed("row_down"):
player.change_row(1)
return
if event.is_action_pressed("drop"):
Actions.drop(player, player.selected, 1 if Input.is_key_pressed(KEY_SHIFT) else 0)
return
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
player.select(player.selected - 1)
player.select_in_row(player.selected % player.HOTBAR - 1)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
player.select(player.selected + 1)
player.select_in_row(player.selected % player.HOTBAR + 1)
if event.is_action_released("attack") and _drawing:
_release_bow()
return

View File

@@ -49,10 +49,26 @@ func held_item_id() -> String:
var s = inventory.get_slot(selected)
return s.id if s != null else ""
## Holds the item in bag slot `i` (any slot: 1-8 pick from the current row,
## K and M move between rows of 8).
func select(i: int) -> void:
selected = wrapi(i, 0, HOTBAR)
selected = wrapi(i, 0, inventory.size())
selected_changed.emit()
## The bag row (0 = the top) the 1-8 keys and the hotbar show.
func row() -> int:
return selected / HOTBAR
func rows() -> int:
return inventory.size() / HOTBAR
## Column `col` of the current row; and the same column a row up or down.
func select_in_row(col: int) -> void:
select(row() * HOTBAR + wrapi(col, 0, HOTBAR))
func change_row(by: int) -> void:
select(wrapi(row() + by, 0, rows()) * HOTBAR + selected % HOTBAR)
## Called by workbenches and chests (container = null for plain crafting).
func open_inventory(container: Node) -> void:
inventory_requested.emit(container)

View File

@@ -274,6 +274,8 @@ func reset_after_death() -> void:
func damage(amount: float, from: Vector3, by: String = "") -> void:
if dead:
return
if player != null and player.get("inventory") != null:
amount *= Actions.charm_mult(player.inventory, by) # (a fang necklace against wolves)
cause = "Killed by %s" % by if not by.is_empty() else "You were killed"
health = maxf(health - amount, 0.0)
_hurt_cd = HURT_NO_REGEN

View File

@@ -12,6 +12,7 @@ var player: CharacterBody3D
var _prompt: Label
var _held_name: Label
var _hotbar: Array = []
var _row_label: Label
var _toasts: VBoxContainer
var _screen: Control
var _menu: Control
@@ -66,6 +67,16 @@ func bind(p: CharacterBody3D) -> void:
bar.add_child(s)
_hotbar.append(s)
root.add_child(bar)
# Which bag row the hotbar is showing (K / M change it), beside the bar.
_row_label = UITheme.label("", 14, UITheme.TEXT_DIM)
_row_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
_row_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
_row_label.grow_vertical = Control.GROW_DIRECTION_BEGIN
_row_label.offset_left = 300
_row_label.offset_right = 420
_row_label.offset_bottom = -40
_row_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
root.add_child(_row_label)
_held_name = UITheme.label("", 15, UITheme.TEXT_DIM)
_held_name.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
@@ -392,9 +403,11 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
func _refresh() -> void:
var first: int = player.row() * _hotbar.size()
for i in range(_hotbar.size()):
_hotbar[i].set_stack(player.inventory.get_slot(i))
_hotbar[i].set_selected(i == player.selected)
_hotbar[i].set_stack(player.inventory.get_slot(first + i))
_hotbar[i].set_selected(first + i == player.selected)
_row_label.text = "Row %d of %d\nK / M" % [player.row() + 1, player.rows()]
var id: String = player.held_item_id()
_held_name.text = ItemDB.item_name(id) if not id.is_empty() else ""

View File

@@ -74,7 +74,7 @@ func bind(p: CharacterBody3D) -> void:
if i == 7:
pass
pv.add_child(grid)
pv.add_child(UITheme.label("Row 1 is your hotbar. Shift-click moves stacks to/from a chest.", 13, UITheme.TEXT_DIM))
pv.add_child(UITheme.label("1-8 pick from the row you're on; K / M change row. Shift-click moves stacks to/from a chest. Q drops what you hold.", 13, UITheme.TEXT_DIM))
var wr := HBoxContainer.new()
wr.add_theme_constant_override("separation", 10)
_worn_label = UITheme.label("", 15)

View File

@@ -13,7 +13,8 @@ const CONTROLS := [
["Left click (fishing rod)", "Cast / reel in when the float dips"],
["R", "Rotate piece (building pieces: quarter turns)"], ["X", "Dismantle the piece you're looking at"],
["E on a bed", "Set spawn point / sleep (night, under a roof)"],
["1 - 8, wheel", "Hotbar"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"],
["1 - 8, wheel", "Hotbar (the bag row you're on)"], ["K / M", "Hotbar shows the bag row above / below"],
["Q (shift+Q)", "Drop what you're holding (just one)"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"],
["Shift + click", "Move stack to/from a chest"], ["Right click", "Split stack"],
["Esc", "This menu (game autosaves every 2 min)"],
]

View File

@@ -30,6 +30,7 @@ const PIECES := {
"forge": preload("res://scripts/building/forge.gd"),
"hearth": preload("res://scripts/building/hearth.gd"),
"rug": preload("res://scripts/building/rug.gd"),
"jar": preload("res://scripts/building/jar.gd"),
"stonecutter": preload("res://scripts/building/stonecutter.gd"),
}
const BuildPiece = preload("res://scripts/building/build_piece.gd")
@@ -268,7 +269,7 @@ func spawn_bed() -> Node3D:
if not has_bed_spawn:
return null
for p in _pieces.get_children():
if p.item_id == "bed" and not p.is_queued_for_deletion() and p.global_position.distance_to(bed_spawn) < 0.3:
if p.is_in_group("bed") and not p.is_queued_for_deletion() and p.global_position.distance_to(bed_spawn) < 0.3:
return p
return null
@@ -299,7 +300,11 @@ func sleep_block_reason(bed: Node3D) -> String:
var h: float = day_night.hours()
if h > 5.5 and h < 19.5:
return "You can only sleep at night (after 19:30)"
if not build.sheltered_at(bed.global_position):
if bed.has_method("open_air") and bed.open_air():
# A bedroll: anywhere, as long as it isn't raining or snowing on you.
if not build.sheltered_at(bed.global_position) and weather.precip > 0.3:
return "Not out here in this weather: find shelter, or wait for it to clear"
elif not build.sheltered_at(bed.global_position):
return "Your bed needs a roof and walls around it"
if wolves != null:
for w in wolves.alive():
@@ -724,7 +729,7 @@ func add_dropped_entry(e: Dictionary) -> Node3D:
func remove_pieces_net(gone: Array, collapsed_nids: Array) -> void:
var any_build := false
for p in gone:
if p.item_id == "bed" and spawn_bed() == p:
if p.is_in_group("bed") and spawn_bed() == p:
has_bed_spawn = false
if p.is_in_group("build"):
any_build = true
@@ -1031,7 +1036,7 @@ signal launched(id: String, origin: Vector3, vel: Vector3, shooter: Node3D)
## support collapses, and its item goes back to `refund_to` if there's room.
## Returns how many pieces collapsed.
func remove_piece(piece: Node3D, refund_to: Node3D = null) -> int:
if piece.item_id == "bed" and spawn_bed() == piece:
if piece.is_in_group("bed") and spawn_bed() == piece:
has_bed_spawn = false
Actions.notify.emit("Spawn point cleared")
var was_build := piece.is_in_group("build")