P2 stone: stonecutter, cut stone, stone floors/walls/pillars/stairs/windows/doorway, hearth; wooden stairs, windows, posts, palisade and gate; stone support rules

This commit is contained in:
Paul Bailey
2026-11-05 01:14:07 +00:00
parent 24c280d885
commit 411bfef34a
16 changed files with 1123 additions and 21 deletions

View File

@@ -61,6 +61,40 @@ static func _define() -> Dictionary:
"snaps": [Vector3(-1, 0, 0), Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(1, 1, 0)],
"center": Vector3(0.33, 0.33, 0),
}
# --- P2 (docs/PROGRESSION_DESIGN.md §3) ---
# Pillar: a 0.4 m post standing on a floor corner or edge, carrying what's
# on top (a floor's corner meets its top).
# (The collider stops 0.2 m short of the top: that's where a floor resting
# on it has its slab, and the overlap check would count it as in the way.)
k["pillar"] = {
"shapes": [_box(Vector3(0, 0.9, 0), Vector3(0.4, 1.8, 0.4))],
"snaps": [Vector3(0, 0, 0), Vector3(0, 2, 0)],
"center": Vector3(0, 1, 0),
}
# Stairs: 2 m wide, climbing 2 m over 4 m toward -Z (the roof's slope):
# low edge on a floor's edge (or the ground), top edge at the next floor
# up. One smooth ramp to walk on; the steps are in the mesh.
var sn := Vector3(0, cos(ROOF_ANGLE), sin(ROOF_ANGLE))
k["stairs"] = {
"shapes": [{"type": "box", "center": Vector3(0, 1, -2) - sn * 0.1,
"size": Vector3(2, 0.2, sqrt(20.0)), "basis": Basis(Vector3.RIGHT, ROOF_ANGLE)}],
"snaps": [Vector3(-1, 0, 0), Vector3(0, 0, 0), Vector3(1, 0, 0),
Vector3(-1, 2, -4), Vector3(0, 2, -4), Vector3(1, 2, -4)],
"center": Vector3(0, 1, -2),
}
# Window: a wall with a 1 x 0.7 m opening, 1 m up.
k["window"] = {
"shapes": [_box(Vector3(0, 0.5, 0), Vector3(2, 1, 0.2)), _box(Vector3(0, 1.85, 0), Vector3(2, 0.3, 0.2)),
_box(Vector3(-0.75, 1.35, 0), Vector3(0.5, 0.7, 0.2)), _box(Vector3(0.75, 1.35, 0), Vector3(0.5, 0.7, 0.2))],
"snaps": wall_snaps,
"center": Vector3(0, 1, 0),
}
# Palisade: 2 m of sharpened logs, 3 m tall, for a ring round your camp.
k["palisade"] = {
"shapes": [_box(Vector3(0, 1.5, 0), Vector3(2, 3, 0.36))],
"snaps": [Vector3(-1, 0, 0), Vector3(0, 0, 0), Vector3(1, 0, 0)],
"center": Vector3(0, 1.5, 0),
}
var n := Vector3(0, cos(ROOF_ANGLE), sin(ROOF_ANGLE)) # panel normal (up and out)
# (Ends exactly at the ridge. The seam where two roofs meet is handled by
# build_system.roofed_at sampling several rays.)
@@ -109,8 +143,16 @@ static func make_shape(s: Dictionary, margin: float = 0.0) -> Array:
static func is_build(def: Dictionary) -> bool:
return def.has("place") and def.place.has("kind")
## "wood" or "stone" (items.json place.material). Stone loses less support
## going up and can't stand on wood (build_system.gd).
var material := "wood"
static func material_of(d: Dictionary) -> String:
return String(d.get("place", {}).get("material", "wood"))
func _make_collision(_mi: MeshInstance3D) -> void:
kind = def.place.kind
material = material_of(def)
collision_layer = 1 | BUILD_LAYER
add_to_group("build")
for s in kinds()[kind].shapes:

View File

@@ -94,21 +94,35 @@ func _grounded(points: Array) -> bool:
return true
return false
static func _loss(from_center: Vector3, to_center: Vector3) -> float:
const STONE_VERTICAL_LOSS := 0.05 ## stone stacks tall (P2)
## Support lost passing from a piece centred at `from_center` to one of
## `material` centred at `to_center`. Through a pillar (`via_pillar`: either
## piece is one) any real step up or down counts as vertical: a pillar under a
## floor's corner carries it straight down to the ground.
static func _loss(from_center: Vector3, to_center: Vector3, material: String = "wood", via_pillar: bool = false) -> float:
var d := to_center - from_center
var horiz := Vector2(d.x, d.z).length()
return VERTICAL_LOSS if absf(d.y) >= horiz * 0.99 else HORIZONTAL_LOSS
if absf(d.y) >= horiz * 0.99 or (via_pillar and absf(d.y) > 0.5):
return STONE_VERTICAL_LOSS if material == "stone" else VERTICAL_LOSS
return HORIZONTAL_LOSS
## Support a piece of `kind` would have at `xf`, given the current structure.
func support_for(kind: String, xf: Transform3D, exclude: Node = null) -> float:
## Wood can't hold stone up (stone on wood has no support from it).
static func _carries(below: Node, material: String) -> bool:
return not (material == "stone" and below.material == "wood")
## Support a piece of `kind` (and `material`) would have at `xf`.
func support_for(kind: String, xf: Transform3D, exclude: Node = null, material: String = "wood") -> float:
var pts := _world_snaps(kind, xf)
if _grounded(pts):
return 1.0
var center: Vector3 = xf * BuildPiece.kinds()[kind].center
var best := 0.0
for n in _touching(pts, exclude):
if not _carries(n, material):
continue
var nc: Vector3 = n.global_transform * BuildPiece.kinds()[n.kind].center
best = maxf(best, n.support * (1.0 - _loss(nc, center)))
best = maxf(best, n.support * (1.0 - _loss(nc, center, material, kind == "pillar" or n.kind == "pillar")))
return best
## Recomputes every piece's support from the ground up. Returns the pieces now
@@ -132,7 +146,8 @@ func recompute() -> Array:
continue
var best := 0.0
for n in nbrs[p]:
best = maxf(best, n.support * (1.0 - _loss(centers[n], centers[p])))
if _carries(n, p.material):
best = maxf(best, n.support * (1.0 - _loss(centers[n], centers[p], p.material, p.kind == "pillar" or n.kind == "pillar")))
if best > p.support + 0.0001:
p.support = best
changed = true
@@ -170,10 +185,19 @@ func check(def: Dictionary, xf: Transform3D, exclude: Node = null) -> String:
if not hits.is_empty():
last_blocker = hits[0].collider
return "Something's in the way"
if support_for(kind, xf, exclude) < MIN_SUPPORT:
var mat := BuildPiece.material_of(def)
if support_for(kind, xf, exclude, mat) < MIN_SUPPORT:
if mat == "stone" and _rests_on_wood(kind, xf, exclude):
return "Stone needs stone or the ground under it (wood can't hold it)"
return "Not enough support"
return ""
func _rests_on_wood(kind: String, xf: Transform3D, exclude: Node) -> bool:
for n in _touching(_world_snaps(kind, xf), exclude):
if n.material == "wood":
return true
return false
## Where a building piece goes when the player aims at `hit` (a ray result
## dictionary: position, normal, collider). `yaw` is the player's preferred
## rotation. Returns {xf, ok, reason, snapped}.
@@ -246,6 +270,8 @@ func _free_xf(kind: String, hp: Vector3, yaw: float, on_terrain: bool) -> Transf
match kind:
"floor":
probe = [Vector3(-1, 0, -1), Vector3(1, 0, -1), Vector3(-1, 0, 1), Vector3(1, 0, 1), Vector3.ZERO]
"pillar":
probe = [Vector3.ZERO]
_:
probe = [Vector3(-1, 0, 0), Vector3.ZERO, Vector3(1, 0, 0)]
var hs: Array[float] = []
@@ -271,9 +297,13 @@ func roofed_at(pos: Vector3) -> bool:
hits += 1
return hits >= 3
## Height of the sideways shelter rays: under a window's sill (1 m), so a
## room with windows still counts as walled in.
const WALL_RAY_Y := 0.6
## 0..1 fraction of 8 horizontal directions blocked by walls within 8 m.
func enclosure_at(pos: Vector3) -> float:
var from := pos + Vector3(0, 1.2, 0)
var from := pos + Vector3(0, WALL_RAY_Y, 0)
var hits := 0
for i in range(8):
var a := TAU * i / 8.0
@@ -284,6 +314,25 @@ func enclosure_at(pos: Vector3) -> float:
func sheltered_at(pos: Vector3) -> bool:
return roofed_at(pos) and enclosure_at(pos) >= 0.74
## 0..1 fraction of 8 horizontal directions whose nearest wall (within 8 m)
## is stone. A stone room keeps you a little warmer (survival.gd).
func stone_walls_at(pos: Vector3) -> float:
var from := pos + Vector3(0, WALL_RAY_Y, 0)
var space := world.get_world_3d().direct_space_state
var hits := 0
for i in range(8):
var a := TAU * i / 8.0
var q := PhysicsRayQueryParameters3D.create(from, from + Vector3(cos(a), 0, sin(a)) * 8.0, BuildPiece.BUILD_LAYER)
var hit := space.intersect_ray(q)
if hit.is_empty():
continue
var c: Object = hit.collider
if c != null and c.has_meta("owner_piece"):
c = c.get_meta("owner_piece")
if c != null and c.get("material") == "stone":
hits += 1
return hits / 8.0
# --- dev helper ---------------------------------------------------------------
## Builds a 4 x 4 m hut through the same snapping a player uses, aiming at the
@@ -291,24 +340,29 @@ func sheltered_at(pos: Vector3) -> bool:
## in hut space), 4 roofs meeting at a ridge, 4 gables. Used by the tests and
## the screenshot views. A coroutine (await it): like a player, it lets a
## physics step pass after each piece so the next overlap check sees it.
## `mat` "stone" builds the floors and walls in stone (a stone doorway, a
## wooden roof on top).
## Returns {"placed": n, "failed": [...], "pieces": [...]}.
func debug_build_hut(center: Vector3, yaw: float) -> Dictionary:
func debug_build_hut(center: Vector3, yaw: float, mat: String = "wood") -> Dictionary:
var out := {"placed": 0, "failed": [], "pieces": []}
var floor_id := mat + "_floor"
var wall_id := mat + "_wall"
var door_id := "stone_doorway" if mat == "stone" else "wood_door"
# Floors: the first sits on the ground, the rest snap to it.
var f0: Node3D = await _hut_place(out, center, yaw, "wood_floor", Vector3(-1, 0, -1), 0.0, null)
var f0: Node3D = await _hut_place(out, center, yaw, floor_id, Vector3(-1, 0, -1), 0.0, null)
if f0 == null:
return out
var top: float = f0.global_position.y - center.y
for l in [Vector3(1, 0, -1), Vector3(-1, 0, 1), Vector3(1, 0, 1)]:
await _hut_place(out, center, yaw, "wood_floor", l, 0.0, null)
await _hut_place(out, center, yaw, floor_id, l, 0.0, null)
# Walls: aim at the floor just inside each edge.
var q := PI * 0.5
for x in [-1.0, 1.0]:
await _hut_place(out, center, yaw, "wood_wall", Vector3(x, top, -1.9), 0.0, f0) # north
await _hut_place(out, center, yaw, "wood_door" if x > 0 else "wood_wall", Vector3(x, top, 1.9), 0.0, f0) # south
await _hut_place(out, center, yaw, wall_id, Vector3(x, top, -1.9), 0.0, f0) # north
await _hut_place(out, center, yaw, door_id if x > 0 else wall_id, Vector3(x, top, 1.9), 0.0, f0) # south
for z in [-1.0, 1.0]:
await _hut_place(out, center, yaw, "wood_wall", Vector3(1.9, top, z), q, f0) # east
await _hut_place(out, center, yaw, "wood_wall", Vector3(-1.9, top, z), q, f0) # west
await _hut_place(out, center, yaw, wall_id, Vector3(1.9, top, z), q, f0) # east
await _hut_place(out, center, yaw, wall_id, Vector3(-1.9, top, z), q, f0) # west
# Roofs on the north and south wall tops, rising to a ridge along X.
for x in [-1.0, 1.0]:
await _hut_place(out, center, yaw, "wood_roof", Vector3(x, top + 2.0, 2.0), 0.0, f0)

View File

@@ -11,7 +11,7 @@ var _panel: AnimatableBody3D
var _tween: Tween
func _visual_mesh() -> Mesh:
return ItemDB.mesh_for("wood_door_frame")
return ItemDB.mesh_for(String(def.get("mesh", item_id)) + "_frame") # (wood_door_frame, stone_door_frame)
func _build() -> void:
_hinge = Node3D.new()

View File

@@ -0,0 +1,21 @@
extends "res://scripts/building/campfire.gd"
## Stone hearth: an indoor fire (docs/PROGRESSION_DESIGN.md §3). Everything a
## campfire does (fuel, cooking, keeping wolves off), but it warms a bigger
## room (1.5x the radius), its chimney keeps the rain off so it never burns
## faster in a storm, and its stone doesn't need a floor of its own.
const HEARTH_RADIUS := HEAT_RADIUS * 1.5
func display_name() -> String:
return "Stone Hearth"
func heat_at(pos: Vector3) -> float:
if not _lit:
return 0.0
return 1.0 - smoothstep(2.0, HEARTH_RADIUS, pos.distance_to(global_position))
func burn_rate() -> float:
return 1.0
func interact_prompt(player: Node) -> String:
return super.interact_prompt(player).replace("Campfire", "Stone hearth")

View File

@@ -0,0 +1,12 @@
extends "res://scripts/building/piece.gd"
## Stonecutter: a saw bench for dressing stone. Unlocks "stonecutter" recipes
## (cut stone, and the stone building pieces) within 6 m. E opens crafting.
func _build() -> void:
add_to_group("station:stonecutter")
func interact_prompt(_player: Node) -> String:
return "Stonecutter [E] Craft"
func interact(player: Node) -> void:
player.open_inventory(null)

View File

@@ -90,6 +90,22 @@ static func build(key: String) -> ArrayMesh:
"charcoal_kiln": m = _kiln()
"smelter": m = _smelter()
"forge": m = _forge()
"cut_stone": m = _cut_stone()
"stonecutter": m = _stonecutter()
"stone_floor": m = _stone_floor()
"stone_wall": m = _stone_wall()
"stone_pillar": m = _stone_pillar()
"stone_stairs": m = _stairs(true)
"stone_window": m = _stone_window()
"stone_doorway": m = _stone_doorway(true)
"stone_doorway_frame": m = _stone_doorway(false)
"stone_hearth": m = _hearth()
"wood_stairs": m = _stairs(false)
"wood_window": m = _wood_window()
"wood_pillar": m = _wood_pillar()
"palisade": m = _palisade()
"palisade_gate": m = _palisade_gate(true)
"palisade_gate_frame": m = _palisade_gate(false)
"berry_bush": m = _berry_bush(true)
"berry_bush_bare": m = _berry_bush(false)
_:
@@ -697,3 +713,227 @@ static func _fish(col: Color, len: float, grilled: bool) -> ArrayMesh:
for s in [-1.0, 1.0]:
P._blob(st, Vector3(len * 0.75, len * 0.38, s * len * 0.11), Vector3.ONE * len * 0.06, n, 0.0, Color(0.05, 0.05, 0.05), r, 0)
return P._commit(st)
# --- P2 stone building and wooden upgrades ---------------------------------------
# Dimensions match the colliders in build_piece.gd (pillar, stairs, window,
# palisade) or reuse the wall/floor/door ones.
const MORTAR := Color(0.36, 0.35, 0.33)
const STONE_BLOCK := Color(0.62, 0.61, 0.58)
static func _block_col(r: RandomNumberGenerator) -> Color:
var c := P._mul(STONE_BLOCK, r.randf_range(0.8, 1.08))
return c.lerp(Color(0.6, 0.55, 0.47), r.randf() * 0.25) # some warmer stones
## Courses of staggered blocks filling x0..x1, y0..y1, `depth` thick, with a
## mortar core behind them so the joints read dark.
static func _blocks(st: SurfaceTool, r: RandomNumberGenerator, x0: float, x1: float, y0: float, y1: float,
depth: float = 0.22, course: float = 0.33) -> void:
if x1 - x0 < 0.02 or y1 - y0 < 0.02:
return
_box(st, Vector3((x0 + x1) * 0.5, (y0 + y1) * 0.5, 0), Vector3(x1 - x0, y1 - y0, depth - 0.04), MORTAR)
var rows := maxi(roundi((y1 - y0) / course), 1)
var h := (y1 - y0) / rows
for i in range(rows):
var y := y0 + h * (i + 0.5)
var x := x0
# Stagger: every other course starts with a half block (world-aligned
# so neighbouring walls line up).
var step := 0.5
var next := x0 + (step * 0.5 if i % 2 == 1 else step)
while x < x1 - 0.01:
var e := minf(next, x1)
_box(st, Vector3((x + e) * 0.5, y, 0), Vector3(e - x - 0.025, h - 0.025, depth), _block_col(r))
x = e
next = e + step * r.randf_range(0.8, 1.2)
static func _cut_stone() -> ArrayMesh:
var st := P._begin()
var r := _rng(211)
_box(st, Vector3(0, 0.08, 0), Vector3(0.3, 0.16, 0.2), _block_col(r))
_box(st, Vector3(0.01, 0.161, 0), Vector3(0.26, 0.004, 0.16), P._mul(STONE_BLOCK, 1.1))
return P._commit(st)
## A heavy bench with a block on it and a frame saw: the stonecutter.
static func _stonecutter() -> ArrayMesh:
var st := P._begin()
var r := _rng(213)
_box(st, Vector3(0, 0.72, 0), Vector3(1.6, 0.12, 0.8), PLANK)
for sx in [-0.7, 0.7]:
for sz in [-0.32, 0.32]:
_box(st, Vector3(sx, 0.33, sz), Vector3(0.14, 0.66, 0.14), PLANK_DARK)
_box(st, Vector3(0, 0.2, 0), Vector3(1.4, 0.08, 0.64), PLANK_DARK)
# Rough stone waiting on the shelf, a block being cut on top.
for i in range(3):
_box(st, Vector3(-0.45 + 0.42 * i, 0.32, 0.02), Vector3(0.3, 0.16, 0.24), _block_col(r), Basis(Vector3.UP, r.randf_range(-0.3, 0.3)))
_box(st, Vector3(0.2, 0.92, 0), Vector3(0.6, 0.28, 0.4), _block_col(r))
# Frame saw: two uprights, a cross bar and the iron blade in the cut.
for sx in [-0.2, 0.62]:
_box(st, Vector3(sx, 1.12, 0), Vector3(0.05, 0.5, 0.05), HANDLE)
_box(st, Vector3(0.21, 1.36, 0), Vector3(0.9, 0.04, 0.04), HANDLE)
_box(st, Vector3(0.21, 1.04, 0), Vector3(0.86, 0.06, 0.012), IRON_BRIGHT)
# Chips and dust.
for i in range(5):
_box(st, Vector3(r.randf_range(-0.7, 0.7), 0.79, r.randf_range(-0.3, 0.3)), Vector3(0.05, 0.03, 0.04), STONE)
return P._commit(st)
## 2 x 2 m flagstones, top at y = 0, on a 0.2 m bed.
static func _stone_floor() -> ArrayMesh:
var st := P._begin()
var r := _rng(217)
_box(st, Vector3(0, -0.11, 0), Vector3(2.0, 0.18, 2.0), MORTAR)
for i in range(4):
for j in range(4):
var c := Vector3(-0.75 + 0.5 * i + r.randf_range(-0.02, 0.02), -0.03, -0.75 + 0.5 * j + r.randf_range(-0.02, 0.02))
_box(st, c, Vector3(0.47, 0.06, 0.47) * Vector3(r.randf_range(0.95, 1.0), 1, r.randf_range(0.95, 1.0)), _block_col(r))
return P._commit(st)
static func _stone_wall() -> ArrayMesh:
var st := P._begin()
_blocks(st, _rng(219), -1.0, 1.0, 0.0, 2.0)
return P._commit(st)
## Square column with a base and a capital, 0.4 m.
static func _stone_pillar() -> ArrayMesh:
var st := P._begin()
var r := _rng(223)
_box(st, Vector3(0, 1.0, 0), Vector3(0.32, 2.0, 0.32), MORTAR)
for i in range(6):
var y := 0.2 + (1.6 / 6.0) * (i + 0.5)
_box(st, Vector3(0, y, 0), Vector3(0.36, 1.6 / 6.0 - 0.025, 0.36), _block_col(r), Basis(Vector3.UP, r.randf_range(-0.04, 0.04)))
_box(st, Vector3(0, 0.1, 0), Vector3(0.44, 0.2, 0.44), _block_col(r))
_box(st, Vector3(0, 1.9, 0), Vector3(0.44, 0.2, 0.44), _block_col(r))
return P._commit(st)
static func _wood_pillar() -> ArrayMesh:
var st := P._begin()
var r := _rng(227)
_box(st, Vector3(0, 1.0, 0), Vector3(0.3, 2.0, 0.3), _board(r, BEAM))
_box(st, Vector3(0, 1.94, 0), Vector3(0.4, 0.12, 0.4), BEAM)
_box(st, Vector3(0, 0.06, 0), Vector3(0.38, 0.12, 0.38), BEAM)
return P._commit(st)
## 8 steps climbing 2 m over 4 m toward -Z (the collider is the smooth ramp
## through their noses). Stone is solid underneath; wood is treads on two
## stringers.
static func _stairs(stone: bool) -> ArrayMesh:
var st := P._begin()
var r := _rng(229 if stone else 231)
var a := 0.4636476
for i in range(8):
var top := 0.25 * (i + 1) - 0.06
var zc := -0.25 - 0.5 * i
if stone:
_box(st, Vector3(0, top * 0.5, zc), Vector3(2.0, top, 0.5), MORTAR)
_box(st, Vector3(0, top - 0.05, zc + 0.01), Vector3(1.96, 0.1, 0.5), _block_col(r))
else:
_box(st, Vector3(0, top - 0.04, zc + 0.03), Vector3(1.7, 0.08, 0.46), _board(r))
if not stone:
var length := sqrt(20.0)
for x in [-0.9, 0.9]:
_box(st, Vector3(x, 0.92, -2.0), Vector3(0.12, 0.26, length), BEAM, Basis(Vector3.RIGHT, a))
return P._commit(st)
## Wall with a 1 x 0.7 m opening 1 m up (x -0.5..0.5, y 1.0..1.7).
static func _stone_window() -> ArrayMesh:
var st := P._begin()
var r := _rng(233)
_blocks(st, r, -1.0, 1.0, 0.0, 1.0)
_blocks(st, r, -1.0, -0.5, 1.0, 1.7, 0.22, 0.35)
_blocks(st, r, 0.5, 1.0, 1.0, 1.7, 0.22, 0.35)
_blocks(st, r, -1.0, 1.0, 1.7, 2.0, 0.22, 0.3)
_box(st, Vector3(0, 0.97, 0.03), Vector3(1.2, 0.07, 0.3), _block_col(r)) # sill
_box(st, Vector3(0, 1.75, 0), Vector3(1.24, 0.12, 0.24), _block_col(r)) # lintel
return P._commit(st)
static func _wood_window() -> ArrayMesh:
var st := P._begin()
var r := _rng(237)
_wall_body(st, r, -0.84, 0.84, 0.1, 1.0)
_wall_body(st, r, -0.84, -0.5, 1.0, 1.7)
_wall_body(st, r, 0.5, 0.84, 1.0, 1.7)
_wall_body(st, r, -0.84, 0.84, 1.7, 1.9)
for x in [-0.92, 0.92]:
_box(st, Vector3(x, 1.0, 0), Vector3(0.16, 2.0, 0.2), BEAM)
for y in [0.06, 1.94]:
_box(st, Vector3(0, y, 0), Vector3(1.68, 0.12, 0.2), BEAM)
# Frame, sill and a shutter swung open on each side (outside, +Z).
_box(st, Vector3(0, 0.98, 0.02), Vector3(1.16, 0.06, 0.26), BEAM)
_box(st, Vector3(0, 1.72, 0), Vector3(1.1, 0.06, 0.2), BEAM)
for side in [-1.0, 1.0]:
_box(st, Vector3(side * 0.52, 1.35, 0), Vector3(0.05, 0.72, 0.2), BEAM)
_box(st, Vector3(side * 0.78, 1.35, 0.14), Vector3(0.48, 0.66, 0.04), _board(r, PLANK_DARK))
_box(st, Vector3(0.0, 1.35, 0), Vector3(0.03, 0.7, 0.03), BEAM) # mullion
return P._commit(st)
## Stone arch for a 1.1 x 1.9 m door (the wooden leaf is wood_door_panel).
static func _stone_doorway(with_panel: bool) -> ArrayMesh:
var st := P._begin()
var r := _rng(239)
_blocks(st, r, -1.0, -0.55, 0.0, 2.0)
_blocks(st, r, 0.55, 1.0, 0.0, 2.0)
_box(st, Vector3(0, 1.97, 0), Vector3(1.14, 0.06, 0.2), MORTAR)
# Arch stones over the opening: a keystone and its neighbours.
for i in range(5):
var x := -0.44 + 0.22 * i
var h := 0.16 + (0.08 if i == 2 else 0.0)
_box(st, Vector3(x, 1.9 + h * 0.5 - 0.02, 0), Vector3(0.2, h, 0.24), _block_col(r))
if with_panel:
_door_panel_into(st, Vector3(-0.54, 0, 0))
return P._commit(st)
## Hearth: the fire sits at the origin in a stone firebox (back toward -Z),
## under a hood and chimney.
static func _hearth() -> ArrayMesh:
var st := P._begin()
var r := _rng(241)
_box(st, Vector3(0, 0.06, 0), Vector3(1.6, 0.12, 1.2), _block_col(r))
_box(st, Vector3(0, 0.125, 0.05), Vector3(0.9, 0.01, 0.7), ASH)
# Back and cheeks.
_box(st, Vector3(0, 0.75, -0.48), Vector3(1.6, 1.3, 0.24), MORTAR)
for i in range(4):
_box(st, Vector3(-0.4 + 0.8 * (i % 2) + r.randf_range(-0.05, 0.05), 0.3 + 0.33 * i, -0.36), Vector3(0.7, 0.3, 0.02), _block_col(r))
for sx in [-0.68, 0.68]:
for i in range(4):
_box(st, Vector3(sx, 0.28 + 0.3 * i, -0.05), Vector3(0.26, 0.28, 0.7), _block_col(r))
# Mantel, hood and chimney.
_box(st, Vector3(0, 1.3, 0.05), Vector3(1.7, 0.12, 0.8), _block_col(r))
_box(st, Vector3(0, 1.55, -0.12), Vector3(1.3, 0.38, 0.6), MORTAR)
_blocks(st, r, -0.3, 0.3, 1.74, 2.0, 0.42, 0.26)
# A few logs in the fire.
for i in range(3):
var a := 0.6 * i - 0.6
var d := Vector3(cos(a), 0, sin(a)) * 0.3
P._cylinder(st, Vector3(0, 0.18, 0.05) - d, Vector3(0, 0.18, 0.05) + d, 0.06, 0.06, 5, P.BARK, r, -1.0, PLANK)
return P._commit(st)
## 2 m of sharpened logs, 3 m tall.
static func _palisade() -> ArrayMesh:
var st := P._begin()
var r := _rng(243)
for i in range(5):
_stake(st, r, -0.8 + 0.4 * i, 0.0, 3.0)
for y in [0.7, 2.1]:
_box(st, Vector3(0, y, -0.2), Vector3(2.0, 0.14, 0.08), BEAM)
return P._commit(st)
static func _stake(st: SurfaceTool, r: RandomNumberGenerator, x: float, y0: float, top: float) -> void:
var h := top - r.randf_range(0.0, 0.18)
var rad := r.randf_range(0.16, 0.19)
P._cylinder(st, Vector3(x, y0, 0), Vector3(x, h - 0.35, 0), rad, rad * 0.95, 7, P.BARK, r)
P._cylinder(st, Vector3(x, h - 0.35, 0), Vector3(x, h, 0), rad * 0.95, 0.02, 7, P.WOOD, r)
## Palisade gate: stakes either side of a 1.1 x 1.9 m opening, more above it.
static func _palisade_gate(with_panel: bool) -> ArrayMesh:
var st := P._begin()
var r := _rng(247)
for x in [-0.8, 0.8]:
_stake(st, r, x, 0.0, 3.0)
for x in [-0.36, 0.0, 0.36]:
_stake(st, r, x, 1.95, 3.0)
for x in [-0.62, 0.62]:
_box(st, Vector3(x, 0.98, 0), Vector3(0.12, 1.96, 0.2), BEAM)
_box(st, Vector3(0, 1.98, 0), Vector3(1.4, 0.12, 0.24), BEAM)
if with_panel:
_door_panel_into(st, Vector3(-0.54, 0, 0))
return P._commit(st)

View File

@@ -557,7 +557,7 @@ func _wall_hit(space: PhysicsDirectSpaceState3D, a: Vector3, b: Vector3) -> bool
var piece: Node = c as Node
while piece != null and piece.get("kind") == null:
piece = piece.get_parent()
if piece != null and String(piece.kind) in ["wall", "door", "gable"]:
if piece != null and String(piece.kind) in ["wall", "door", "gable", "window", "palisade", "pillar"]:
return true
ex.append(hit.rid) # a floor or roof edge: look past it
return false

View File

@@ -73,6 +73,9 @@ func balanced_diet() -> bool:
return false
return true
var stone_room := false ## sheltered by mostly stone walls (P2): a little warmer
const STONE_ROOM_WARMTH := 6.0
var player: Node3D ## the local player, or a server_player.gd on a server
var world: Node3D
var _stamina_wait := 0.0
@@ -92,6 +95,7 @@ func step(dt: float) -> void:
_shelter_left = 0.25
roofed = world.build.roofed_at(p)
sheltered = roofed and world.build.enclosure_at(p) >= 0.74
stone_room = sheltered and world.build.stone_walls_at(p) >= 0.5
# Rain soaks you unless there's a roof over your head.
if not roofed and world.weather.rain_at(p) > 0.25:
wet = WET_SECONDS
@@ -105,6 +109,8 @@ func step(dt: float) -> void:
rested = maxf(rested - dt, 0.0)
var wear: Dictionary = ItemDB.get_item(player.worn).get("wear", {}) if not player.worn.is_empty() else {}
var target: float = world.air_warmth(p, sheltered) + heat * 70.0 + float(wear.get("warmth", 0.0))
if stone_room:
target += STONE_ROOM_WARMTH
if wet > 0.0:
target -= 22.0 * float(wear.get("wet_chill", 1.0))
wet = maxf(wet - dt * (3.0 if heat > 0.3 else 1.0), 0.0) # fires dry you off

View File

@@ -255,7 +255,7 @@ func _rebuild_recipes() -> void:
for c in _recipe_list.get_children():
c.queue_free()
var stations: Array[String] = []
for st in ["workbench", "campfire", "forge"]:
for st in ["workbench", "campfire", "forge", "stonecutter"]:
if Actions.station_near(player, st):
stations.append(st)
_station_label.text = "Nearby: " + (", ".join(stations) if not stations.is_empty() else "nothing (hand crafting only)")

View File

@@ -27,6 +27,8 @@ const PIECES := {
"kiln": preload("res://scripts/building/processor.gd"),
"smelter": preload("res://scripts/building/processor.gd"),
"forge": preload("res://scripts/building/forge.gd"),
"hearth": preload("res://scripts/building/hearth.gd"),
"stonecutter": preload("res://scripts/building/stonecutter.gd"),
}
const BuildPiece = preload("res://scripts/building/build_piece.gd")
const UITheme = preload("res://scripts/ui/theme.gd")