- BuildOps.place_mirrored / update_mirrored / remove_mirrored /
inside_match_bounds: pure Statics in build_ops.gd, sym-Kopplung aus M1,
gleiche Snap-Regeln (Paritaet bleibt), tiefe Kopien (kein Aliasing).
- V9-Grenzen scharf: laege der Zwilling ausserhalb der Match-Grenzen
(Loader-Regel _inside_bounds ±1 m), wird er NICHT gespeichert und der
Status meldet V9 im Klartext — auch kein halber sym-Link.
- SpawnTool-Spiegelung im GEGNER-Team (Blau West <-> Rot Ost = faire
Gegenstelle, Radius 1:1), Partner-Map in design_notes.spawn_mirror
(Loader-frei, rundtrip-sicher). Auf der Mitte: nicht gedoppelt.
- build_tool.gd: duenne Hooks an den 6 Klick-Mutationen + demo_place —
EIN Undo-Schritt nimmt Original UND Zwilling zurueck.
- spawn_tool.gd: Hooks in setzen/verschieben/loeschen/demo_place/demo_move;
_sym_mode() defensiv ueber main.get() (Mock-Mains ohne sym_mode -> OFF,
statt SCRIPT ERROR — Lektion aus test_shortcuts).
- Neue Suite tests/test_build_spawn_mirror.gd (76 Checks ALL OK) +
Probe tests/probe_m4_mirror_render.gd (2 Beweis-Bilder).
- Regression: 19/20 Suiten gruen; test_glb_view 15/25 FAIL pre-existing
(per git-stash gegen 437f6bf verifiziert, Ursache unabhaengig vom Diff).
440 lines
20 KiB
GDScript
440 lines
20 KiB
GDScript
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)
|