extends SceneTree ## Gitea #7: growing a house. A longer one (along the ridge): take out the ## end wall and gables, add floors, walls and a roof bay, and the gables go ## on the new end. A wider one (across the ridge): the roof gets a second row ## of panels on top of the first, and its taller ends close with a half wall ## under a gable. Inside both you're under a roof and sheltered. ## Run: godot --headless --path . --script res://tests/test_roofs.gd var _fails := 0 var _finished := false var S: Node var w: Node3D var B: Node const Q := PI * 0.5 func check(cond: bool, what: String) -> void: print((" ok " if cond else " FAIL ") + what) if not cond: _fails += 1 func _initialize() -> void: _run.call_deferred() func _run() -> void: S = root.get_node("Saves") S.root_dir = "user://test_roofs_worlds" for x in S.list_worlds(): S.delete_world(x.id) S.current_id = S.create_world("Roofs", 1337) w = load("res://scenes/world.tscn").instantiate() root.add_child(w) for _i in range(8): await physics_frame w.creatures.enabled = false w.player.set_physics_process(false) w.player.survival.set_process(false) B = w.build await _longer() await _wider() await _six() print("an L: a wing off the side of the hut") var l: Dictionary = await _l_shape() print(" (wing walls failed: %s)" % [l.get("walls_failed", [])]) check(l.failed.is_empty() and l.placed == 8, "the wing's roof and gables, where it meets the hut too (%d placed%s)" % [l.placed, "" if l.failed.is_empty() else ": %s" % [l.failed]]) if l.has("c"): var inside := Vector3(l.c.x, l.f0.global_position.y, l.c.z + 4.0) check(B.roofed_at(inside) and B.sheltered_at(inside), "sheltered in the wing (roof %s, walls %.2f)" % [B.roofed_at(inside), B.enclosure_at(inside)]) print("haphazard: a wider wing, its roof carried on over the hut's") var o: Dictionary = await _odd_wing() check(o.has("floors_walls") and o.floors_walls.failed.is_empty(), "(the wing's floors and walls: %s)" % [o.get("floors_walls", {}).get("failed", o.failed)]) check(o.failed.is_empty() and o.placed == 9, "its roof goes on, the last bay over the hut's roof (%d placed%s)" % [o.placed, "" if o.failed.is_empty() else ": %s" % [o.failed]]) if o.has("c"): var want := [Vector3(1, 2, 3), Vector3(1, 2, -3), Vector3(1, 3, 0)] var top: float = o.top check(want.all(func(v): return o.where.any(func(p): return p.distance_to(Vector3(v.x, top + v.y, v.z)) < 0.05)), "(that bay is where it's meant to be, over the hut's roof)") print("roofs through roofs") var hut2: Array = B.build_pieces().filter(func(p): return p.kind == "roof") var r0: Node3D = hut2[0] var roof_def: Dictionary = DB().get_item("wood_roof") var across := Transform3D(r0.global_basis.rotated(Vector3.UP, Q), r0.global_transform * Vector3(0, 0.5, -1.0)) check(B.check(roof_def, across) != "Something's in the way", "a roof crossing another at right angles isn't in its way (%s)" % B.check(roof_def, across)) check(B.check(roof_def, r0.global_transform) == "There's already a roof there", "but not one roof inside another, the same way round") var wall_def: Dictionary = DB().get_item("wood_wall") check(B.check(wall_def, Transform3D(r0.global_basis, r0.global_transform * Vector3(0, -0.4, -1.0))) == "Something's in the way", "and a wall still can't go through a roof") _finished = true check(_finished, "test ran to the end (no script errors)") for x in S.list_worlds(): S.delete_world(x.id) DirAccess.remove_absolute(S.root_dir) print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails]) quit(1 if _fails > 0 else 0) ## One piece, aimed as a player would (build_system.gd's hut helper). func _place(out: Dictionary, c: Vector3, id: String, local: Vector3, ryaw: float, on: Node3D) -> Node3D: return await B._hut_place(out, c, 0.0, id, local, ryaw, on) func DB() -> Node: return root.get_node("ItemDB") func _new_out() -> Dictionary: return {"placed": 0, "failed": [], "pieces": []} func _longer() -> void: print("a longer house (along the ridge)") var c: Vector3 = B.debug_find_site(w._spawn + Vector3(20, 0, 0), 5.0) var hut: Dictionary = await B.debug_build_hut(c, 0.0) check(hut.failed.is_empty(), "the 4 x 4 hut (%d pieces)" % hut.placed) var f0: Node3D = hut.pieces[0] var top: float = f0.global_position.y - c.y for p in hut.pieces.duplicate(): var l: Vector3 = p.global_position - c if (p.kind == "wall" or p.kind == "gable") and l.x > 1.5: w.remove_piece(p) for _i in range(3): await physics_frame var out := _new_out() for z in [-1.0, 1.0]: await _place(out, c, "wood_floor", Vector3(3, 0, z), 0.0, null) await _place(out, c, "wood_wall", Vector3(3, top, -1.9), 0.0, f0) await _place(out, c, "wood_wall", Vector3(3, top, 1.9), 0.0, f0) for z in [-1.0, 1.0]: await _place(out, c, "wood_wall", Vector3(3.9, top, z), Q, f0) await _place(out, c, "wood_roof", Vector3(3, top + 2.0, 2.0), 0.0, f0) await _place(out, c, "wood_roof", Vector3(3, top + 2.0, -2.0), PI, f0) await _place(out, c, "wood_gable", Vector3(4, top + 2.0, 1.0), Q, f0) await _place(out, c, "wood_gable", Vector3(4, top + 2.0, -1.0), -Q, f0) check(out.failed.is_empty() and out.placed == 10, "a bay on the end, and its gables (%d placed%s)" % [out.placed, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var inside := Vector3(c.x + 3.0, f0.global_position.y, c.z) check(B.roofed_at(inside) and B.sheltered_at(inside), "sheltered in the new end") func _wider() -> void: print("a wider house (across the ridge): 4 m along, 8 m across") var c := Vector3.INF for near in [Vector3(-30, 0, 0), Vector3(0, 0, 30), Vector3(30, 0, 25), Vector3(-25, 0, -25), Vector3(0, 0, -35)]: var at: Vector3 = w._spawn + near var got: Vector3 = B.debug_find_site(at, 5.0) if got != at: # (it hands back where you asked when there's no site) c = got break check(c != Vector3.INF, "(a site for it)") if c == Vector3.INF: return var out := _new_out() # Floors: 2 x 4. The first sits on the ground, the rest snap to it. var f0: Node3D = await _place(out, c, "wood_floor", Vector3(-1, 0, -3), 0.0, null) check(f0 != null, "(the first floor: %s)" % [out.failed]) if f0 == null: return var top: float = f0.global_position.y - c.y for x in [-1.0, 1.0]: for z in [-3.0, -1.0, 1.0, 3.0]: if x == -1.0 and z == -3.0: continue await _place(out, c, "wood_floor", Vector3(x, 0, z), 0.0, null) # Walls all round. for x in [-1.0, 1.0]: await _place(out, c, "wood_wall", Vector3(x, top, -3.9), 0.0, f0) await _place(out, c, "wood_wall", Vector3(x, top, 3.9), 0.0, f0) for z in [-3.0, -1.0, 1.0, 3.0]: await _place(out, c, "wood_wall", Vector3(1.9, top, z), Q, f0) await _place(out, c, "wood_wall", Vector3(-1.9, top, z), Q, f0) check(out.failed.is_empty(), "floors and walls (%d placed%s)" % [out.placed, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var n0: int = out.placed # The roof: the first row on the long walls, the second row on top of it, # meeting at the ridge 2 m up. for x in [-1.0, 1.0]: await _place(out, c, "wood_roof", Vector3(x, top + 2.0, 4.0), 0.0, f0) await _place(out, c, "wood_roof", Vector3(x, top + 2.0, -4.0), PI, f0) var n1: int = out.placed for x in [-1.0, 1.0]: await _place(out, c, "wood_roof", Vector3(x, top + 3.0, 2.0), 0.0, f0) await _place(out, c, "wood_roof", Vector3(x, top + 3.0, -2.0), PI, f0) check(out.placed - n1 == 4 and out.failed.is_empty(), "a second row of roof on top of the first, up to the ridge (%d placed%s)" % [out.placed - n1, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var ridge: Array = out.pieces.filter(func(p): return p.kind == "roof" and absf(p.global_position.y - (f0.global_position.y + 3.0)) < 0.2) check(ridge.size() == 4 and ridge.all(func(p): return p.support >= B.MIN_SUPPORT), "(held up: support %s)" % [ridge.map(func(p): return snappedf(p.support, 0.01))]) # The ends: a gable on the wall at the eaves; nearer the ridge, a half # wall on the wall and a gable on the half wall. var n2: int = out.placed for x in [-2.0, 2.0]: await _place(out, c, "wood_gable", Vector3(x, top + 2.0, 3.0), Q, f0) await _place(out, c, "wood_gable", Vector3(x, top + 2.0, -3.0), -Q, f0) await _place(out, c, "wood_half_wall", Vector3(x, top + 2.0, 1.0), Q, f0) await _place(out, c, "wood_half_wall", Vector3(x, top + 2.0, -1.0), Q, f0) await _place(out, c, "wood_gable", Vector3(x, top + 3.0, 1.0), Q, f0) await _place(out, c, "wood_gable", Vector3(x, top + 3.0, -1.0), -Q, f0) check(out.placed - n2 == 12 and out.failed.is_empty(), "both ends closed: gables, and half walls under the upper gables (%d placed%s)" % [out.placed - n2, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var halves: Array = out.pieces.filter(func(p): return p.kind == "half_wall") check(halves.size() == 4 and halves.all(func(p): return absf(p.global_position.y - (f0.global_position.y + 2.0)) < 0.05), "the half walls stand on the walls' tops") var uppers: Array = out.pieces.filter(func(p): return p.kind == "gable" and absf(p.global_position.y - (f0.global_position.y + 3.0)) < 0.05) check(uppers.size() == 4, "and the upper gables stand on the half walls (%d)" % uppers.size()) for z in [-3.0, 0.0, 3.0]: var inside := Vector3(c.x, f0.global_position.y, c.z + z) check(B.roofed_at(inside) and B.sheltered_at(inside), "sheltered inside, %+.0f m across (roof %s, walls %.2f)" % [z, B.roofed_at(inside), B.enclosure_at(inside)]) # A half wall on its own: a low wall on a floor. var out2 := _new_out() var c2: Vector3 = B.debug_find_site(w._spawn + Vector3(-25, 0, 10), 3.0) var g0: Node3D = await _place(out2, c2, "wood_floor", Vector3(0, 0, 0), 0.0, null) if g0 != null: var t2: float = g0.global_position.y - c2.y var hw: Node3D = await _place(out2, c2, "wood_half_wall", Vector3(0, t2, 0.9), 0.0, g0) check(hw != null and absf(hw.global_position.y - g0.global_position.y) < 0.05, "a half wall on a floor's edge: a low wall (%s)" % [out2.failed]) ## A site with room for a house `half` metres each way from its middle. func _site(half: float, tries: Array) -> Vector3: for near in tries: var at: Vector3 = w._spawn + near var got: Vector3 = B.debug_find_site(at, half) if got != at: return got return Vector3.INF func _six() -> void: print("a 6 x 6 house: a roof ridge bridges the middle") var c := _site(4.0, [Vector3(30, 0, -20), Vector3(-30, 0, 20), Vector3(25, 0, 30), Vector3(-35, 0, -10)]) check(c != Vector3.INF, "(a site for it)") if c == Vector3.INF: return var out := _new_out() var f0: Node3D = await _place(out, c, "wood_floor", Vector3(-2, 0, -2), 0.0, null) if f0 == null: check(false, "(the first floor: %s)" % [out.failed]) return var top: float = f0.global_position.y - c.y for x in [-2.0, 0.0, 2.0]: for z in [-2.0, 0.0, 2.0]: if not (x == -2.0 and z == -2.0): await _place(out, c, "wood_floor", Vector3(x, 0, z), 0.0, null) for x in [-2.0, 0.0, 2.0]: await _place(out, c, "wood_wall", Vector3(x, top, -2.9), 0.0, f0) await _place(out, c, "wood_wall", Vector3(x, top, 2.9), 0.0, f0) for z in [-2.0, 0.0, 2.0]: await _place(out, c, "wood_wall", Vector3(2.9, top, z), Q, f0) await _place(out, c, "wood_wall", Vector3(-2.9, top, z), Q, f0) check(out.failed.is_empty() and out.placed == 21, "9 floors and 12 walls (%d placed%s)" % [out.placed, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var n0: int = out.placed for x in [-2.0, 0.0, 2.0]: await _place(out, c, "wood_roof", Vector3(x, top + 2.0, 3.0), 0.0, f0) await _place(out, c, "wood_roof", Vector3(x, top + 2.0, -3.0), PI, f0) check(out.placed - n0 == 6, "a row of roof up from each long wall, leaving a 2 m gap in the middle (%d%s)" % [out.placed - n0, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var n1: int = out.placed for x in [-2.0, 0.0, 2.0]: await _place(out, c, "wood_roof_ridge", Vector3(x, top + 3.0, 0.0), 0.0, f0) check(out.placed - n1 == 3 and out.failed.is_empty(), "roof ridges bridge it (%d placed%s)" % [out.placed - n1, "" if out.failed.is_empty() else ": %s" % [out.failed]]) var ridges: Array = out.pieces.filter(func(p): return p.kind == "roof_ridge") check(ridges.size() == 3 and ridges.all(func(p): return absf(p.global_position.y - (f0.global_position.y + 3.0)) < 0.05 and absf(p.global_position.z - c.z) < 0.05), "on the rows' top edges, along the middle") check(ridges.all(func(p): return p.support >= B.MIN_SUPPORT), "(held up: support %s)" % [ridges.map(func(p): return snappedf(p.support, 0.01))]) var n2: int = out.placed for x in [-3.0, 3.0]: await _place(out, c, "wood_gable", Vector3(x, top + 2.0, 2.0), Q, f0) await _place(out, c, "wood_gable", Vector3(x, top + 2.0, -2.0), -Q, f0) await _place(out, c, "wood_half_wall", Vector3(x, top + 2.0, 0.0), Q, f0) await _place(out, c, "wood_ridge_gable", Vector3(x, top + 3.0, 0.0), Q, f0) check(out.placed - n2 == 8 and out.failed.is_empty(), "the ends closed: gables, and a half wall and ridge gable in the middle (%d placed%s)" % [out.placed - n2, "" if out.failed.is_empty() else ": %s" % [out.failed]]) for z in [-2.0, 0.0, 2.0]: var inside := Vector3(c.x, f0.global_position.y, c.z + z) check(B.roofed_at(inside) and B.sheltered_at(inside), "sheltered inside, %+.0f m across (roof %s, walls %.2f)" % [z, B.roofed_at(inside), B.enclosure_at(inside)]) ## An L: a wing off the side of the 4 x 4 hut, its ridge at right angles to ## the hut's. Returns {placed, failed} for the wing's roof and gables. func _l_shape() -> Dictionary: var c := _site(4.5, [Vector3(-30, 0, -30), Vector3(35, 0, 10), Vector3(-10, 0, 40), Vector3(20, 0, -35), Vector3(-40, 0, 5), Vector3(10, 0, 45), Vector3(45, 0, -10), Vector3(-20, 0, -45)]) if c == Vector3.INF: return {"placed": 0, "failed": ["no site"]} var hut: Dictionary = await B.debug_build_hut(c, 0.0) var f0: Node3D = hut.pieces[0] var top: float = f0.global_position.y - c.y var out := _new_out() # The wing: 4 m wide, 4 m out from the hut's south wall (z = 2 to 6). for x in [-1.0, 1.0]: for z in [3.0, 5.0]: await _place(out, c, "wood_floor", Vector3(x, 0, z), 0.0, null) for z in [3.0, 5.0]: await _place(out, c, "wood_wall", Vector3(1.9, top, z), Q, f0) await _place(out, c, "wood_wall", Vector3(-1.9, top, z), Q, f0) for x in [-1.0, 1.0]: await _place(out, c, "wood_wall", Vector3(x, top, 5.9), 0.0, f0) var walls_failed: Array = out.failed.duplicate() var roof := _new_out() # Its roof: ridge along z, panels up from the east and west walls. for z in [3.0, 5.0]: await _place(roof, c, "wood_roof", Vector3(2.0, top + 2.0, z), Q, f0) await _place(roof, c, "wood_roof", Vector3(-2.0, top + 2.0, z), -Q, f0) # The far end's gables, and the gables where the wing meets the hut (on # the hut's south wall, under the wing's roof). for zz in [6.0, 2.0]: await _place(roof, c, "wood_gable", Vector3(1.0, top + 2.0, zz), 0.0, f0) await _place(roof, c, "wood_gable", Vector3(-1.0, top + 2.0, zz), PI, f0) return {"placed": roof.placed, "failed": roof.failed, "walls_failed": walls_failed, "c": c, "f0": f0} ## Haphazard: a 6 m-wide wing off the east end of the 4 x 4 hut. Its roof ## is taller than the hut's, so to close the gap where they meet, its roof ## carries on one bay over the hut's roof. func _odd_wing() -> Dictionary: var c := _site(4.5, [Vector3(40, 0, 30), Vector3(-45, 0, -20), Vector3(30, 0, 40), Vector3(-15, 0, -50), Vector3(50, 0, 0), Vector3(-50, 0, 15), Vector3(0, 0, 50), Vector3(15, 0, -50)]) if c == Vector3.INF: return {"placed": 0, "failed": ["no site"]} var hut: Dictionary = await B.debug_build_hut(c, 0.0) var f0: Node3D = hut.pieces[0] var top: float = f0.global_position.y - c.y var out := _new_out() for x in [3.0, 5.0]: for z in [-2.0, 0.0, 2.0]: await _place(out, c, "wood_floor", Vector3(x, 0, z), 0.0, null) for x in [3.0, 5.0]: await _place(out, c, "wood_wall", Vector3(x, top, -2.9), 0.0, f0) await _place(out, c, "wood_wall", Vector3(x, top, 2.9), 0.0, f0) for z in [-2.0, 0.0, 2.0]: await _place(out, c, "wood_wall", Vector3(5.9, top, z), Q, f0) var roof := _new_out() # The wing's roof (6 m across: a row each side and a ridge), and one bay # more, at x = 1, through the hut's own roof. for x in [5.0, 3.0, 1.0]: await _place(roof, c, "wood_roof", Vector3(x, top + 2.0, 3.0), 0.0, f0) await _place(roof, c, "wood_roof", Vector3(x, top + 2.0, -3.0), PI, f0) await _place(roof, c, "wood_roof_ridge", Vector3(x, top + 3.0, 0.0), 0.0, f0) var where: Array = roof.pieces.map(func(p): return (p.global_position - c).snapped(Vector3(0.01, 0.01, 0.01))) return {"placed": roof.placed, "failed": roof.failed, "floors_walls": out, "c": c, "f0": f0, "where": where, "top": top}