extends SceneTree ## Editor-Tests (headless): Karte t_97ea59c5 — Bauen-Modus (Modular-Builder). ## Ausführen: godot --headless --path . --script res://tests/test_bauen.gd ## Exit 0 = alles grün. Läuft OHNE UI (BuildOps/SvmapIO/ActionHistory) — ## GUI-Feinheiten (Ghost, Panel) verifiziert der Desktop-Smoke (--bauen-demo). var failures := 0 var checks := 0 func _init() -> void: _cleanup() _run_tests() _cleanup() if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1) func _cleanup() -> void: for path in ["user://test_bauen.svmap", "user://test_bauen_legacy.svmap"]: if FileAccess.file_exists(path): DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func _check(cond: bool, label: String) -> void: checks += 1 if cond: print(" ok: %s" % label) else: failures += 1 print(" FAIL: %s" % label) func _run_tests() -> void: _test_catalog() _test_make_piece_isolation() _test_piece_meshes() _test_snap() _test_bake_and_ray_pick() _test_undo_redo() _test_save_load_roundtrip() _test_legacy_map_without_gebaeude() # ------------------------------------------------------------------ Tests --- func _test_catalog() -> void: print("Katalog") _check(BuildOps.KINDS.size() == 10, "10 Bauteil-Typen im Katalog") for kind_id in BuildOps.KINDS.keys(): var kind: Dictionary = BuildOps.KINDS[kind_id] _check(str(kind.get("label", "")).length() > 3, "Label für %s" % str(kind_id)) _check((kind.get("punkte", []) as Array).size() >= 2, "Punkte für %s" % str(kind_id)) _check((kind.get("raster", "") == "ecke") or (kind.get("raster", "") == "mitte"), "Raster-Typ für %s" % str(kind_id)) # Abnahme-Typen vorhanden: Fundament/Wand/Dach/Treppe/Tür for need in ["fundament", "wand", "dach_giebel", "treppe", "tuer"]: _check(BuildOps.KINDS.has(need), "Katalog enthält %s" % need) func _test_make_piece_isolation() -> void: print("make_piece") var teil := BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 90.0) _check(not teil.is_empty(), "Bauteil erzeugt") _check(str(teil["id"]) == "wand", "id übernommen") # pos kommt aus einem Vector3 (float32!) — Vergleiche mit Toleranz. _check(absf(float(teil["pos"][0]) - 2.0) < 0.001 and absf(float(teil["pos"][1]) - 0.2) < 0.001 \ and absf(float(teil["pos"][2]) - 4.0) < 0.001, "Position übernommen") _check(float(teil["rot"]) == 90.0, "Drehung übernommen") # Punkte sind KOPIEN (kein Aliasing mit dem Katalog) var before_x: float = (BuildOps.KINDS["wand"]["punkte"] as Array)[1][0] (teil["punkte"] as Array)[1][0] = 99.0 _check(float((BuildOps.KINDS["wand"]["punkte"] as Array)[1][0]) == before_x, "Katalog-Punkte bleiben unangetastet (keine Alias-Falle)") var tuer := BuildOps.make_piece("tuer", Vector3.ZERO, 0.0) _check((tuer["details"] as Array).size() == 1, "Tür-Wand hat 1 Öffnung") _check(int(((tuer["details"] as Array)[0] as Dictionary)["typ"]) == BuildOps.DETAIL_DOOR, "Öffnung ist eine Tür") _check(BuildOps.make_piece("gibts_nicht", Vector3.ZERO, 0.0).is_empty(), "Unbekannte ID → leeres Teil (kein Crash)") func _test_piece_meshes() -> void: print("Meshes") for kind_id in BuildOps.KINDS.keys(): var mesh := BuildOps.piece_mesh(BuildOps.make_piece(str(kind_id), Vector3.ZERO, 0.0)) _check(mesh != null and mesh.get_surface_count() > 0, "Mesh für %s" % str(kind_id)) var wand := BuildOps.piece_mesh(BuildOps.make_piece("wand", Vector3.ZERO, 0.0)) var tuer := BuildOps.piece_mesh(BuildOps.make_piece("tuer", Vector3.ZERO, 0.0)) var wand_tris := int(wand.get_faces().size() / 3) var tuer_tris := int(tuer.get_faces().size() / 3) _check(wand_tris > 0 and tuer_tris > 0, "Wand/Tür-Meshes haben Dreiecke") _check(wand_tris != tuer_tris, "Tür-Öffnung verändert das Wand-Mesh (%d vs %d Tris)" % [wand_tris, tuer_tris]) # Fundament: 2×2×0,2 m — AABB prüfen (lokale Geometrie, Ursprung Fußpunkt) var f := BuildOps.piece_mesh(BuildOps.make_piece("fundament", Vector3.ZERO, 0.0)) var size := f.get_aabb().size _check(absf(size.x - 2.0) < 0.01 and absf(size.z - 2.0) < 0.01 and absf(size.y - 0.2) < 0.01, "Fundament-AABB 2×0,2×2 m (ist %.2f×%.2f×%.2f)" % [size.x, size.y, size.z]) func _test_snap() -> void: print("Raster-Snap") # "ecke" (Wand) → gerade Meter; "mitte" (Fundament) → ungerade Meter. var w := BuildOps.snapped_pos("wand", Vector3(2.7, 0.11, -3.4)) _check(w.x == 2.0 and w.z == -4.0, "Wand rastet auf Zellen-Ecken (2, −4)") _check(absf(w.y - 0.10) < 0.001, "Y rastet auf 5 cm") var f := BuildOps.snapped_pos("fundament", Vector3(2.7, 0.0, 3.2)) _check(f.x == 3.0 and f.z == 3.0, "Fundament rastet auf Zellen-Mitte (3, 3)") # Verbund: Fundament Mitte (3,3) hat Ecken bei 2 und 4 → Wand auf (2, y, 4) # grenzt bündig. (Konvention, dokumentiert in build_ops.gd.) func _test_bake_and_ray_pick() -> void: print("Baken + Ray-Pick") var doc := SvmapDocument.create_new("Bautest", 256.0, "mittag") # Kleines Haus: Fundament → 3 Wände + Tür-Wand → 2 Giebelwände → Dach + Treppe var pieces: Array = BuildOps.ensure_block(doc) pieces.append(BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0)) pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 2.0), 0.0)) # Süd pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 0.0)) # Nord pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 90.0)) # West pieces.append(BuildOps.make_piece("tuer", Vector3(4.0, 0.2, 4.0), 90.0)) # Ost + Tür pieces.append(BuildOps.make_piece("giebelwand", Vector3(2.0, 2.2, 2.0), 0.0)) pieces.append(BuildOps.make_piece("giebelwand", Vector3(2.0, 2.2, 4.0), 0.0)) pieces.append(BuildOps.make_piece("dach_giebel", Vector3(3.0, 2.2, 3.0), 0.0)) pieces.append(BuildOps.make_piece("treppe", Vector3(10.0, 0.0, 9.0), 0.0)) var baked := BuildOps.bake_faces(doc) var tris := int((baked["faces"] as PackedVector3Array).size() / 3) _check(tris > 100, "Haus gebacken: %d Dreiecke" % tris) _check((baked["offsets"] as Array).size() == 9, "Offsets je Teil (9)") _check(int(baked["offsets"][0]) == 0, "erstes Teil startet bei Dreieck 0") _check(int(baked["offsets"][8]) < tris, "letztes Teil liegt im Bereich") # Ray von oben → trifft Dach (Teil-Index 7) var pick := BuildOps.ray_pick(baked, Vector3(3.0, 50.0, 3.0), Vector3(0.0, -1.0, 0.0)) _check(not pick.is_empty(), "Ray von oben trifft das Haus") _check(int(pick["piece"]) >= 0 and int(pick["piece"]) < 9, "Treffer gehört zu einem Teil (Index %d)" % int(pick["piece"])) # Ray von der Seite auf Wandhöhe, daneben der Türöffnung → Teil 4 (Tür-Wand) var side := BuildOps.ray_pick(baked, Vector3(50.0, 1.0, 3.6), Vector3(-1.0, 0.0, 0.0)) _check(not side.is_empty() and int(side["piece"]) == 4, "Seiten-Ray trifft die Tür-Wand (Index %s)" % str(side.get("piece", "?"))) # … und DURCH die Türöffnung hindurch auf die West-Wand (Teil 3) var door := BuildOps.ray_pick(baked, Vector3(50.0, 1.0, 3.0), Vector3(-1.0, 0.0, 0.0)) _check(not door.is_empty() and int(door["piece"]) == 3, "Ray durch die Türöffnung trifft die West-Wand (Index %s)" % str(door.get("piece", "?"))) # Leer → kein Treffer _check(BuildOps.ray_pick({"faces": PackedVector3Array(), "offsets": []}, Vector3.ZERO, Vector3.DOWN).is_empty(), "Leeres Baken → kein Treffer") # AABB eines Teils liegt an der richtigen Stelle (Dach über 2,2 m) var aabb := BuildOps.piece_aabb(pieces[7]) _check(aabb.position.y > 2.0, "Dach-AABB liegt über den Wänden (y=%.2f)" % aabb.position.y) func _test_undo_redo() -> void: print("Undo/Redo") var doc := SvmapDocument.create_new("Undo", 256.0, "mittag") var hist := ActionHistory.new() hist.begin(doc) # Teil 1: erster Snapshot MUSS den (leeren) Block enthalten, damit Strg+Z # das erste Bauteil komplett entfernen kann. var before: Dictionary = hist.snapshot(["gebaeude"]) _check(before.has("gebaeude"), "Snapshot enthält den leeren gebaeude-Block") BuildOps.ensure_block(doc).append(BuildOps.make_piece("fundament", Vector3.ZERO, 0.0)) hist.commit("Fundament (2×2 m) gesetzt", before, hist.snapshot(["gebaeude"])) # Teil 2 + 3 for i in 2: before = hist.snapshot(["gebaeude"]) BuildOps.ensure_block(doc).append(BuildOps.make_piece("wand", Vector3(float(i) * 2.0, 0.2, 0.0), 0.0)) hist.commit("Wand (2 m) gesetzt", before, hist.snapshot(["gebaeude"])) _check(BuildOps.piece_count(doc) == 3, "3 Bauteile gesetzt") hist.undo() _check(BuildOps.piece_count(doc) == 2, "Undo → 2 Bauteile") hist.undo() hist.undo() _check(BuildOps.piece_count(doc) == 0, "3× Undo → alles weg (inkl. erstes Teil)") hist.redo() hist.redo() _check(BuildOps.piece_count(doc) == 2, "2× Redo → 2 Bauteile wieder da") # Löschen + Undo (wie build_tool.delete_selected) before = hist.snapshot(["gebaeude"]) (BuildOps.pieces(doc) as Array).remove_at(0) hist.commit("Bauteil gelöscht", before, hist.snapshot(["gebaeude"])) _check(BuildOps.piece_count(doc) == 1, "Löschen → 1 Teil") hist.undo() _check(BuildOps.piece_count(doc) == 2, "Undo holt gelöschtes Teil zurück") func _test_save_load_roundtrip() -> void: print("Speichern/Laden") var doc := SvmapDocument.create_new("Bauen-Save", 256.0, "mittag") var pieces: Array = BuildOps.ensure_block(doc) pieces.append(BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0)) pieces.append(BuildOps.make_piece("tuer", Vector3(2.0, 0.2, 2.0), 90.0)) pieces.append(BuildOps.make_piece("dach_pyramide", Vector3(3.0, 2.2, 3.0), 0.0)) var save := SvmapIO.save_to_file("user://test_bauen.svmap", doc) _check(bool(save["ok"]), "Map mit Bauteilen speichert (ohne Fehler)") var load := SvmapIO.load_from_file("user://test_bauen.svmap") _check(bool(load["ok"]), "Map mit Bauteilen lädt (V1–V15 + Prüfsumme grün)") var loaded: Dictionary = load["data"] _check(BuildOps.piece_count(loaded) == 3, "3 Bauteile überleben .svmap") var src: Dictionary = pieces[1] var dst: Dictionary = (BuildOps.pieces(loaded) as Array)[1] _check(str(dst["id"]) == "tuer" and float(dst["rot"]) == 90.0, "Bauteil-Felder identisch") _check((dst["punkte"] as Array) == (src["punkte"] as Array), "Punkte byte-gleich über den Roundtrip") _check(((dst["details"] as Array)[0] as Dictionary)["w"] == 1.0, "Tür-Öffnung überlebt den Roundtrip") # Gebackenes Mesh aus GELADENEN Teilen = wie vor dem Speichern (deterministisch) var before_faces: PackedVector3Array = BuildOps.bake_faces(doc)["faces"] var after_faces: PackedVector3Array = BuildOps.bake_faces(loaded)["faces"] _check(before_faces == after_faces, "Baken ist deterministisch (vor/nach Speichern identisch)") func _test_legacy_map_without_gebaeude() -> void: print("Legacy-Maps") var doc := SvmapDocument.create_new("Legacy", 256.0, "mittag") doc.erase("gebaeude") # alte Map ohne die Sektion var save := SvmapIO.save_to_file("user://test_bauen_legacy.svmap", doc) _check(bool(save["ok"]), "Map OHNE gebaeude speichert") var load := SvmapIO.load_from_file("user://test_bauen_legacy.svmap") _check(bool(load["ok"]), "Map OHNE gebaeude lädt") _check(BuildOps.piece_count(load["data"]) == 0, "Legacy-Map: 0 Bauteile, kein Crash") if FileAccess.file_exists("user://test_bauen_legacy.svmap"): DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_bauen_legacy.svmap"))