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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
21
scripts/building/hearth.gd
Normal file
21
scripts/building/hearth.gd
Normal 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")
|
||||
12
scripts/building/stonecutter.gd
Normal file
12
scripts/building/stonecutter.gd
Normal 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)
|
||||
Reference in New Issue
Block a user