extends "res://scripts/building/piece.gd" ## A snap-together building piece: wood floor, wall, roof, gable or door frame. ## ## Geometry lives here (not in items.json) because it has to match the meshes ## in item_meshes.gd exactly: ## - `shapes`: the colliders, also used (shrunk slightly) for overlap checks. ## - `snaps`: snap points in local space. Two pieces are connected when a snap ## point of one coincides with a snap point of the other; build_system.gd ## uses that for snapping and for structural support. ## - `center`: rough centre of mass, used to tell vertical from sideways ## support. ## ## All pieces are 2 m on a side so they line up on a 2 m grid. ## Origins: floor = centre of the top surface; wall, door and gable = bottom ## centre; roof = centre of the low edge, rising 1 m over 2 m toward -Z ## (about 27 degrees), so two roofs from opposite walls of a 4 m hut meet at a ## ridge. const BUILD_LAYER := 4 ## physics layer 3: shelter + regrowth queries const ROOF_ANGLE := 0.4636476 ## atan(1 / 2) const ROOF_T := 0.12 ## roof panel thickness static var _kinds := {} var kind := "wall" var support := 1.0 ## 0..1, set by build_system.gd var grounded := false static func kinds() -> Dictionary: if _kinds.is_empty(): _kinds = _define() return _kinds static func _define() -> Dictionary: var k := {} var wall_snaps := [ Vector3(-1, 0, 0), Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(-1, 2, 0), Vector3(0, 2, 0), Vector3(1, 2, 0)] k["floor"] = { "shapes": [_box(Vector3(0, -0.1, 0), Vector3(2, 0.2, 2))], "snaps": [Vector3(-1, 0, -1), Vector3(0, 0, -1), Vector3(1, 0, -1), Vector3(-1, 0, 0), Vector3(1, 0, 0), Vector3(-1, 0, 1), Vector3(0, 0, 1), Vector3(1, 0, 1)], "center": Vector3(0, -0.1, 0), } k["wall"] = { "shapes": [_box(Vector3(0, 1, 0), Vector3(2, 2, 0.2))], "snaps": wall_snaps, "center": Vector3(0, 1, 0), } # Door frame: 1.1 m x 1.9 m opening (the player is 0.8 m x 1.8 m). k["door"] = { "shapes": [_box(Vector3(-0.775, 1, 0), Vector3(0.45, 2, 0.2)), _box(Vector3(0.775, 1, 0), Vector3(0.45, 2, 0.2)), _box(Vector3(0, 1.95, 0), Vector3(1.1, 0.1, 0.2))], "snaps": wall_snaps, "center": Vector3(0, 1, 0), } # Gable: right triangle, 0 m tall at -X, 1 m at +X (fills the end of a # roof; rotate 180 degrees for the other half). k["gable"] = { "shapes": [_prism([Vector2(-1, 0), Vector2(1, 0), Vector2(1, 1)], 0.2)], "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.) k["roof"] = { "shapes": [{"type": "box", "center": Vector3(0, 0.5, -1) + n * ROOF_T * 0.5, "size": Vector3(2, ROOF_T, sqrt(5.0)), "basis": Basis(Vector3.RIGHT, ROOF_ANGLE)}], "snaps": [Vector3(-1, 0, 0), Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(-1, 1, -2), Vector3(0, 1, -2), Vector3(1, 1, -2)], "center": Vector3(0, 0.5, -1), } return k static func _box(c: Vector3, s: Vector3) -> Dictionary: return {"type": "box", "center": c, "size": s, "basis": Basis()} ## A prism: a 2D outline in the XY plane, extruded `t` along Z (centred). static func _prism(outline: Array, t: float) -> Dictionary: return {"type": "prism", "outline": outline, "thickness": t} ## Builds a Shape3D for one shape entry, shrunk by `margin` on every side ## (margin > 0 is for overlap tests, so touching neighbours don't count). ## Returns [shape, local transform]. static func make_shape(s: Dictionary, margin: float = 0.0) -> Array: if s.type == "box": var b := BoxShape3D.new() var size: Vector3 = s.size b.size = Vector3(maxf(size.x - margin * 2.0, 0.02), maxf(size.y - margin * 2.0, 0.02), maxf(size.z - margin * 2.0, 0.02)) return [b, Transform3D(s.basis, s.center)] var outline: Array = s.outline var c := Vector2.ZERO for p in outline: c += p c /= outline.size() var pts := PackedVector3Array() var hz := maxf(float(s.thickness) * 0.5 - margin, 0.01) for p: Vector2 in outline: var q := p if margin > 0.0: q = c + (p - c) * (1.0 - margin * 2.0 / maxf((p - c).length(), 0.01)) pts.append(Vector3(q.x, q.y, -hz)) pts.append(Vector3(q.x, q.y, hz)) var cv := ConvexPolygonShape3D.new() cv.points = pts return [cv, Transform3D()] 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: var made := make_shape(s) var cs := CollisionShape3D.new() cs.shape = made[0] cs.transform = made[1] add_child(cs) func snap_points() -> Array: var out := [] for p in kinds()[kind].snaps: out.append(global_transform * p) return out func display_name() -> String: return ItemDB.item_name(item_id) func interact_prompt(_player: Node) -> String: if support < 0.999: return "%s (support %d%%)" % [display_name(), roundi(support * 100.0)] return display_name()