extends SceneTree ## Editor-Tests (headless): Karte t_ffa207b1 — M4 Bau-Teile + Spawnpunkte ## spiegeln (Spec /opt/data/scopedvr/KARTEN_MIRROR_vc27.md, Mathe aus M1 ## symmetry_core.gd). Ausführen: ## godot --headless --path . --script res://tests/test_build_spawn_mirror.gd ## Exit 0 = alles grün. Läuft OHNE UI (BuildOps/SpawnOps-Statics + ## ActionHistory) — GUI-Feinheiten (Statuszeile, Ghost) verifiziert der ## Desktop-Smoke, nicht hier. ## ## Abdeckung (Karten-Auftrag): ## · Bau-Teil setzen → Zwilling vorhanden (Punkt, Achse X, Achse Z) ## · Bau-Teil entfernen → beide weg ## · Verschieben/Drehen/Anheben spiegelt den Zwilling mit ## · Spawn setzen → gespiegelter Spawn (Gegner-Team, faire Gegenstelle) ## · Spawn nahe der Grenze → V9-Meldung statt Speichern ## · Modus AUS = Gegenprobe (Verhalten exakt wie bisher) ## · EIN Undo-Schritt nimmt Original + Zwilling zurück var failures := 0 var checks := 0 const OFF := SymmetryCore.Mode.OFF const POINT := SymmetryCore.Mode.POINT const AXIS_X := SymmetryCore.Mode.AXIS_X const AXIS_Z := SymmetryCore.Mode.AXIS_Z # spawn_tool.gd hat bewusst KEINEN class_name (Node-Werkzeug) — Statics via preload. const SpawnTool := preload("res://src/editor/spawn_tool.gd") func _init() -> void: _run_tests() if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1) 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_build_place_point() _test_build_place_axes() _test_build_snap_parity() _test_build_remove() _test_build_update_follows() _test_build_v9_bounds() _test_build_off_gegenprobe() _test_build_one_undo_step() _test_spawn_add_mirror() _test_spawn_move_delete_mirror() _test_spawn_v9_bounds() _test_spawn_off_gegenprobe() _test_spawn_roundtrip_safe() func _new_doc() -> Dictionary: # 256-m-Karte: bounds ±128 zentriert → Kartenmitte (0, 0). return SvmapDocument.create_new("Sym-M4-Test", 256.0, "mittag") func _piece_at(doc: Dictionary, index: int) -> Dictionary: var list := BuildOps.pieces(doc) if index < 0 or index >= list.size() or not (list[index] is Dictionary): return {} return list[index] as Dictionary func _pos3(p: Dictionary) -> Vector3: var pos: Array = p.get("pos", [0.0, 0.0, 0.0]) return Vector3(float(pos[0]), float(pos[1]), float(pos[2])) func _near(a: float, b: float) -> bool: return absf(a - b) < 0.001 # ------------------------------------------------------- Bau-Teile: setzen --- func _test_build_place_point() -> void: print("Testgruppe: Bau-Teil setzen → Zwilling (Punktspiegelung)") var doc := _new_doc() var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.2, 20.0), 90.0) BuildOps.ensure_block(doc).append(teil) var msg: String = BuildOps.place_mirrored(doc, teil, POINT) _check(msg == "", "Punktspiegelung: still gespiegelt (keine Meldung)") _check(BuildOps.piece_count(doc) == 2, "2 Bauteile nach dem Setzen") var twin := _piece_at(doc, 1) _check(str(twin.get("id", "")) == "wand", "Zwilling hat gleiche kind_id") _check(_near(_pos3(twin).x, -10.0) and _near(_pos3(twin).y, 0.2) and _near(_pos3(twin).z, -20.0), "Zwilling-Position gespiegelt (−10, 0.2, −20)") _check(_near(float(twin.get("rot", 0.0)), 270.0), "Zwilling-Drehung 90° + 180° = 270°") var key := SymmetryCore.sym_of(teil) _check(not key.is_empty() and SymmetryCore.sym_of(twin) == key, "beide Teile tragen dieselbe sym-Kennung") # V16-Realität: GENAU 2 Einträge pro Kennung. var n := 0 for t in BuildOps.pieces(doc): if SymmetryCore.sym_of(t as Dictionary) == key: n += 1 _check(n == 2, "sym-Kennung verbindet genau 2 Teile") # Zwilling ist eine tiefe Kopie (keine Alias-Falle bei punkte). var first_x: float = (teil["punkte"] as Array)[0][0] (teil["punkte"] as Array)[0][0] = 123.0 _check(_near(float((twin["punkte"] as Array)[0][0]), first_x), "Zwilling-Punkte sind Kopien (kein Aliasing)") func _test_build_place_axes() -> void: print("Testgruppe: Bau-Teil setzen → Zwilling (Achse X / Achse Z)") var doc := _new_doc() var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) BuildOps.ensure_block(doc).append(teil) BuildOps.place_mirrored(doc, teil, AXIS_X) var twin := _piece_at(doc, 1) _check(_near(_pos3(twin).x, -10.0) and _near(_pos3(twin).z, 20.0), "Achse X: x gespiegelt, z bleibt") _check(_near(float(twin.get("rot", 0.0)), 270.0), "Achse X: Drehung −90° = 270°") var doc2 := _new_doc() var teil2 := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) BuildOps.ensure_block(doc2).append(teil2) BuildOps.place_mirrored(doc2, teil2, AXIS_Z) var twin2 := _piece_at(doc2, 1) _check(_near(_pos3(twin2).x, 10.0) and _near(_pos3(twin2).z, -20.0), "Achse Z: z gespiegelt, x bleibt") _check(_near(float(twin2.get("rot", 0.0)), 270.0), "Achse Z: Drehung −90° = 270°") # Auf der Mitte: nicht gedoppelt (Spec vc27). var doc3 := _new_doc() var mitte := BuildOps.make_piece("wand", Vector3(0.0, 0.0, 0.0), 0.0) BuildOps.ensure_block(doc3).append(mitte) var msg: String = BuildOps.place_mirrored(doc3, mitte, POINT) _check(msg.contains("nicht gedoppelt"), "Bauteil auf der Mitte: Meldung statt Zwilling") _check(BuildOps.piece_count(doc3) == 1, "Auf der Mitte: nur 1 Bauteil") _check(SymmetryCore.sym_of(mitte).is_empty(), "Auf der Mitte: kein sym-Link") ## Gleiche Snap-Regeln: Spiegelung erhält die Raster-Parität ## (Raster „ecke" = gerade Meter, „mitte" = ungerade Meter) bei zentrierter Karte. func _test_build_snap_parity() -> void: print("Testgruppe: Snap-Parität bleibt erhalten") var doc := _new_doc() var ecke := BuildOps.make_piece("wand", Vector3(2.0, 0.0, 4.0), 0.0) BuildOps.ensure_block(doc).append(ecke) BuildOps.place_mirrored(doc, ecke, POINT) _check(_near(_pos3(_piece_at(doc, 1)).x, -2.0) and _near(_pos3(_piece_at(doc, 1)).z, -4.0), "ecke-Teil: gerade Meter bleiben gerade (−2, −4)") var doc2 := _new_doc() var mitte := BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0) BuildOps.ensure_block(doc2).append(mitte) BuildOps.place_mirrored(doc2, mitte, POINT) _check(_near(_pos3(_piece_at(doc2, 1)).x, -3.0) and _near(_pos3(_piece_at(doc2, 1)).z, -3.0), "mitte-Teil: ungerade Meter bleiben ungerade (−3, −3)") # ----------------------------------------------------- Bau-Teile: entfernen --- func _test_build_remove() -> void: print("Testgruppe: Bau-Teil entfernen → beide weg") var doc := _new_doc() var teil := BuildOps.make_piece("boden", Vector3(11.0, 0.0, 21.0), 0.0) BuildOps.ensure_block(doc).append(teil) BuildOps.place_mirrored(doc, teil, POINT) _check(BuildOps.piece_count(doc) == 2, "Vorbereitung: 2 Bauteile") var msg: String = BuildOps.remove_mirrored(doc, 0, POINT) _check(BuildOps.piece_count(doc) == 0, "Original löschen → Zwilling auch weg") _check(msg.contains("Zwilling"), "Status meldet den mitgelöschten Zwilling") # Umgekehrt: Zwilling löschen (Index 1) → Original mit weg. var doc2 := _new_doc() var teil2 := BuildOps.make_piece("boden", Vector3(11.0, 0.0, 21.0), 0.0) BuildOps.ensure_block(doc2).append(teil2) BuildOps.place_mirrored(doc2, teil2, POINT) BuildOps.remove_mirrored(doc2, 1, POINT) _check(BuildOps.piece_count(doc2) == 0, "Zwilling löschen → Original auch weg") # Paar + Fremdteil: Löschung des Fremdteils lässt das Paar unangetastet. var doc3 := _new_doc() var a := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 0.0), 0.0) BuildOps.ensure_block(doc3).append(a) BuildOps.place_mirrored(doc3, a, POINT) var fremd := BuildOps.make_piece("boden", Vector3(30.0, 0.0, 0.0), 0.0) BuildOps.ensure_block(doc3).append(fremd) BuildOps.remove_mirrored(doc3, 2, POINT) _check(BuildOps.piece_count(doc3) == 2, "Fremdteil weg, Paar bleibt (2 Teile)") _check(not _piece_at(doc3, 0).is_empty() and not _piece_at(doc3, 1).is_empty(), "Paar-Indizes unverändert verschoben") # ------------------------------------- Bau-Teile: verschieben/drehen/anheben --- func _test_build_update_follows() -> void: print("Testgruppe: Verschieben/Drehen/Anheben — Zwilling folgt") var doc := _new_doc() var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) BuildOps.ensure_block(doc).append(teil) BuildOps.place_mirrored(doc, teil, POINT) # Verschieben (Pfeiltaste-Muster): Original auf neue Position, dann sync. teil["pos"] = [14.0, 0.0, 20.0] BuildOps.update_mirrored(doc, 0, POINT) var twin := _piece_at(doc, 1) _check(_near(_pos3(twin).x, -14.0) and _near(_pos3(twin).z, -20.0), "Zwilling folgt der verschobenen Position gespiegelt") # Drehen: +90° am Original → Zwilling = gespiegelte NEUE Drehung. teil["rot"] = 180.0 BuildOps.update_mirrored(doc, 0, POINT) _check(_near(float((_piece_at(doc, 1) as Dictionary).get("rot", 0.0)), 0.0), "Zwilling-Drehung: 180° + 180° = 0° (modulo 360)") # Anheben: y wandert 1:1 mit (Höhe ist ein Skalar). teil["pos"] = [14.0, 2.4, 20.0] BuildOps.update_mirrored(doc, 0, POINT) _check(_near(_pos3(_piece_at(doc, 1)).y, 2.4), "Zwilling-y folgt 1:1 (Skalar)") # Teil OHNE sym-Partner (außerhalb des Modus gebaut) bleibt unberührt. var doc2 := _new_doc() var solo := BuildOps.make_piece("wand", Vector3(6.0, 0.0, 0.0), 0.0) BuildOps.ensure_block(doc2).append(solo) _check(BuildOps.update_mirrored(doc2, 0, POINT) == "", "Teil ohne sym: kein Zwilling, keine Meldung") _check(BuildOps.piece_count(doc2) == 1, "Teil ohne sym: nichts wurde gedoppelt") # ------------------------------------------------------- Bau-Teile: V9 ------- ## V9-Realität (deshalb so getestet): bei zentrierten bounds bildet die ## Spiegelung gültiger Elemente bildet IMMER auf gültige Stellen ab — der V9-Fall ## entsteht, wenn das ORIGINAL bereits außerhalb liegt (Editor erzwingt die ## Grenzen beim Nudge/Baken nicht) oder Grenzen asymmetrisch sind. Dann ## verweigert der Zwilling die Speicherung mit Klartext-Meldung. func _test_build_v9_bounds() -> void: print("Testgruppe: Bau-Teil — V9-Grenzen scharf") var doc := _new_doc() # Original außerhalb (z = 200 > 128 + 1 Toleranz): auch der Zwilling läge # außerhalb → Meldung statt still zu speichern. var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 200.0), 0.0) BuildOps.ensure_block(doc).append(teil) var msg: String = BuildOps.place_mirrored(doc, teil, POINT) _check(msg.contains("V9"), "V9-Meldung statt still zu speichern") _check(msg.contains("Match-Grenzen"), "Meldung nennt die Match-Grenzen") _check(BuildOps.piece_count(doc) == 1, "V9-Fall: NUR das Original gespeichert") _check(SymmetryCore.sym_of(teil).is_empty(), "V9-Fall: kein halber sym-Link zurückbleibt") # Grenz-Nähe INNERHALB (Toleranz ±1 m): spiegeln bleibt erlaubt. var doc2 := _new_doc() var nah := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 127.5), 0.0) BuildOps.ensure_block(doc2).append(nah) _check(BuildOps.place_mirrored(doc2, nah, POINT) == "", "nahe der Grenze (|z| ≤ 128 + 1): gespiegelt ok") _check(BuildOps.piece_count(doc2) == 2, "Grenz-Nähe: Zwilling vorhanden") # update_mirrored: Original aus den Grenzen verschoben → Zwilling bleibt unverändert. var doc3 := _new_doc() var t3 := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 30.0), 0.0) BuildOps.ensure_block(doc3).append(t3) BuildOps.place_mirrored(doc3, t3, POINT) t3["pos"] = [10.0, 85.0, 30.0] # y = 85 > 80 + 1 → gespiegelt ebenfalls draußen var msg3: String = BuildOps.update_mirrored(doc3, 0, POINT) _check(msg3.contains("V9"), "update: V9-Meldung statt Zwilling verschieben") _check(_near(_pos3(_piece_at(doc3, 1)).z, -30.0) and _near(_pos3(_piece_at(doc3, 1)).y, 0.0), "update-V9: Zwilling blieb UNVERÄNDERT (−30 / y 0)") # ------------------------------------------------- Bau-Teile: Modus AUS ----- func _test_build_off_gegenprobe() -> void: print("Testgruppe: Modus AUS = Verhalten wie bisher (Bau-Teile)") var doc := _new_doc() var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) BuildOps.ensure_block(doc).append(teil) _check(BuildOps.place_mirrored(doc, teil, OFF) == "", "OFF: keine Meldung, kein Zwilling") _check(BuildOps.piece_count(doc) == 1, "OFF: nur 1 Bauteil") _check(SymmetryCore.sym_of(teil).is_empty(), "OFF: kein sym-Feld gesetzt") teil["pos"] = [12.0, 0.0, 20.0] BuildOps.update_mirrored(doc, 0, OFF) _check(BuildOps.piece_count(doc) == 1, "OFF: Verschieben dupliziert nicht") var msg: String = BuildOps.remove_mirrored(doc, 0, OFF) _check(BuildOps.piece_count(doc) == 0 and msg == "", "OFF: Löschen entfernt genau 1 Teil") ## EIN Undo-Schritt: Original + Zwilling springen zusammen zurück. func _test_build_one_undo_step() -> void: print("Testgruppe: Bau-Teile — EIN Undo-Schritt für beide Seiten") var doc := _new_doc() var hist := ActionHistory.new() hist.begin(doc) var before: Dictionary = hist.snapshot(["gebaeude"]) var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) BuildOps.ensure_block(doc).append(teil) BuildOps.place_mirrored(doc, teil, POINT) hist.commit("Wand gesetzt", before, hist.snapshot(["gebaeude"])) _check(BuildOps.piece_count(doc) == 2, "commit: beide Teile drin") hist.undo() _check(BuildOps.piece_count(doc) == 0, "EIN undo: Original UND Zwilling weg") hist.redo() _check(BuildOps.piece_count(doc) == 2, "EIN redo: beide wieder da") # -------------------------------------------------------- Spawns: setzen ----- func _test_spawn_add_mirror() -> void: print("Testgruppe: Spawn setzen → gespiegelter Spawn (Gegner-Team)") var doc := _new_doc() var point := SpawnOps.add_zone(doc, 0, -100.0, 40.0) var pid := str(point["id"]) var res: Dictionary = SpawnTool.spawn_mirror_add(doc, 0, pid, POINT) _check(bool(res["ok"]), "Spiegelung ok: %s" % str(res["status"])) _check(SpawnOps.count_points(doc, 1) == 5, "Team Rot: 4 Default + 1 Zwilling") var twin := _spawn_by_id(doc, 1, spawn_partner_id(doc, pid)) _check(not twin.is_empty(), "gespiegelter Spawn vorhanden") _check(_near(_pos3(twin).x, 100.0) and _near(_pos3(twin).z, -40.0), "Zwilling-Position gespiegelt (100, −40)") _check(_near(_pos3(twin).y, _pos3(point).y), "Zwilling-y = Terrain-Höhe am gespiegelten Punkt") _check(_near(SpawnOps.zone_radius(doc, str(twin["id"])), SpawnOps.zone_radius(doc, pid)), "Zwilling-Radius = Original-Radius") _check(spawn_partner_id(doc, pid) == str(twin["id"]) and spawn_partner_id(doc, str(twin["id"])) == pid, "Partner-Map verlinkt beide Richtungen") # Achse X: z bleibt gleich. var doc2 := _new_doc() var p2 := SpawnOps.add_zone(doc2, 1, 100.0, 40.0) SpawnTool.spawn_mirror_add(doc2, 1, str(p2["id"]), AXIS_X) var twin2 := _spawn_by_id(doc2, 0, spawn_partner_id(doc2, str(p2["id"]))) _check(_near(_pos3(twin2).x, -100.0) and _near(_pos3(twin2).z, 40.0), "Achse X: x gespiegelt, z bleibt") # Auf der Mitte: nicht gedoppelt. var doc3 := _new_doc() var p3 := SpawnOps.add_zone(doc3, 0, 0.0, 0.0) var res3: Dictionary = SpawnTool.spawn_mirror_add(doc3, 0, str(p3["id"]), POINT) _check(str(res3["status"]).contains("nicht gedoppelt"), "Spawn auf der Mitte: Meldung") _check(SpawnOps.count_points(doc3, 1) == 4, "Spawn auf der Mitte: kein Zwilling") # ------------------------------------------- Spawns: verschieben / löschen ---- func _test_spawn_move_delete_mirror() -> void: print("Testgruppe: Spawn verschieben/löschen — Zwilling folgt") var doc := _new_doc() var point := SpawnOps.add_zone(doc, 0, -100.0, 40.0) var pid := str(point["id"]) SpawnTool.spawn_mirror_add(doc, 0, pid, POINT) var twin_id := spawn_partner_id(doc, pid) # Verschieben des Originals → Zwilling auf gespiegelte NEUE Position. SpawnOps.move_point(doc, 0, pid, -60.0, 20.0) var msg: String = SpawnTool.spawn_mirror_move(doc, 0, pid, POINT) _check(msg.contains("mitverschoben"), "Verschieben gespiegelt: %s" % msg) var twin := _spawn_by_id(doc, 1, twin_id) _check(_near(_pos3(twin).x, 60.0) and _near(_pos3(twin).z, -20.0), "Zwilling auf gespiegelter neuer Position (60, −20)") # Löschen des Originals (macht delete_selected) → Zwilling weg + Map sauber. SpawnOps.remove_point(doc, 0, pid) var del_msg: String = SpawnTool.spawn_mirror_delete(doc, pid, POINT) _check(del_msg.contains("mit entfernt"), "Löschen gespiegelt: %s" % del_msg) _check(_spawn_by_id(doc, 1, twin_id).is_empty(), "Zwilling-Spawn weg") _check(SpawnOps.count_points(doc, 0) == 4 and SpawnOps.count_points(doc, 1) == 4, "beide Teams wieder bei 4 Default-Zonen") _check(SpawnTool.spawn_mirror_partner(doc, pid).is_empty(), "Partner-Map-Einträge entfernt (kein Geist)") # Zwilling löschen (andere Richtung) → Original mit weg. var doc2 := _new_doc() var p2 := SpawnOps.add_zone(doc2, 1, 100.0, -40.0) var pid2 := str(p2["id"]) SpawnTool.spawn_mirror_add(doc2, 1, pid2, POINT) var orig_id := spawn_partner_id(doc2, pid2) SpawnTool.spawn_mirror_delete(doc2, pid2, POINT) _check(_spawn_by_id(doc2, 0, orig_id).is_empty(), "Zwilling gelöscht → Original mit weg") # --------------------------------------------------------- Spawns: V9 -------- func _test_spawn_v9_bounds() -> void: print("Testgruppe: Spawn nahe der Grenze → V9-Meldung statt Speichern") # Original außerhalb der Grenzen (z = 130 > 128 + 1): der gespiegelte # Zwilling läge ebenfalls draußen → Meldung, NICHT speichern. var doc := _new_doc() var point := SpawnOps.add_zone(doc, 0, -100.0, 130.0) var pid := str(point["id"]) var res: Dictionary = SpawnTool.spawn_mirror_add(doc, 0, pid, POINT) _check(not bool(res["ok"]), "ok=false — Zwilling NICHT gespeichert") _check(str(res["status"]).contains("V9"), "Status nennt V9") _check(str(res["status"]).contains("Match-Grenzen"), "Status nennt die Match-Grenzen") _check(SpawnOps.count_points(doc, 1) == 4, "Team Rot unverändert (4 Default)") _check(SpawnTool.spawn_mirror_partner(doc, pid).is_empty(), "kein halber Partner-Link") # Grenz-Nähe INNERHALB: spiegeln bleibt erlaubt (Toleranz ±1 m). var doc2 := _new_doc() var nah := SpawnOps.add_zone(doc2, 0, -100.0, 127.5) var res2: Dictionary = SpawnTool.spawn_mirror_add(doc2, 0, str(nah["id"]), POINT) _check(bool(res2["ok"]), "nahe der Grenze (|z| ≤ 129): gespiegelt ok") _check(SpawnOps.count_points(doc2, 1) == 5, "Grenz-Nähe: Zwilling vorhanden") # ---------------------------------------------------- Spawns: Modus AUS ------ func _test_spawn_off_gegenprobe() -> void: print("Testgruppe: Modus AUS = Verhalten wie bisher (Spawns)") var doc := _new_doc() var point := SpawnOps.add_zone(doc, 0, -100.0, 40.0) var pid := str(point["id"]) var res: Dictionary = SpawnTool.spawn_mirror_add(doc, 0, pid, OFF) _check(bool(res["ok"]) and str(res["status"]) == "", "OFF: still nichts gespiegelt") _check(SpawnOps.count_points(doc, 1) == 4, "OFF: Team Rot unverändert") SpawnOps.move_point(doc, 0, pid, -60.0, 20.0) _check(SpawnTool.spawn_mirror_move(doc, 0, pid, OFF) == "", "OFF: Verschieben spiegelt nicht") SpawnOps.remove_point(doc, 0, pid) var del_res: String = SpawnTool.spawn_mirror_delete(doc, pid, OFF) _check(del_res == "" and SpawnOps.count_points(doc, 0) == 4, "OFF: Löschen entfernt nur das Original") ## design_notes bleibt Loader-frei + rundtrip-sicher (Radius-Muster). func _test_spawn_roundtrip_safe() -> void: print("Testgruppe: Spawn-Spiegelung überlebt Speichern/Laden") var doc := _new_doc() (doc["info"] as Dictionary)["author"] = "Sly" var point := SpawnOps.add_zone(doc, 0, -100.0, 40.0) var pid := str(point["id"]) SpawnTool.spawn_mirror_add(doc, 0, pid, POINT) var save: Dictionary = SvmapIO.save_to_file("user://test_build_spawn_mirror.svmap", doc) _check(bool(save["ok"]), "save_to_file ok: %s" % str(save["errors"])) var load: Dictionary = SvmapIO.load_from_file("user://test_build_spawn_mirror.svmap") _check(bool(load["ok"]), "load_from_file ok (Prüfsumme stimmt)") var back: Dictionary = load["data"] _check(SpawnOps.count_points(back, 1) == 5, "Re-Import: gespiegelter Spawn noch da") var notes: Dictionary = back.get("design_notes", {}) _check((notes.get(SpawnTool.SPAWN_MIRROR_KEY, {}) as Dictionary).has(pid), "Re-Import: Partner-Link in design_notes überlebt") # ------------------------------------------------------------------ Helfer --- func _spawn_by_id(doc: Dictionary, team: int, pid: String) -> Dictionary: if pid.is_empty(): return {} for p in SpawnOps.points(doc, team): if p is Dictionary and str(p.get("id", "")) == pid: return p as Dictionary return {} func spawn_partner_id(doc: Dictionary, pid: String) -> String: return SpawnTool.spawn_mirror_partner(doc, pid)