302 lines
13 KiB
GDScript
302 lines
13 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): Karte t_ffa207b1 — M4 Bauteile (jetzt normale Objekte) + 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 (BauOps/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://core/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_replace_follows()
|
||
_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 _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
|
||
|
||
|
||
# ---------------------------------------- Bauteile (normale Objekte) --------
|
||
|
||
func _flach() -> Callable:
|
||
return func(_x: float, _z: float) -> float: return 0.0
|
||
|
||
|
||
## Wand per echter Snap-Logik setzen (Hit knapp neben der Kantenmitte).
|
||
func _setze(doc: Dictionary, oid: String, hit: Vector3, mode: int, gid: int, spiegel := -1) -> Dictionary:
|
||
var snap := BuildSnap.berechne(oid, hit, 0.0, BuildSnap.sammle_teile(doc), _flach())
|
||
return BauOps.setze_teil(doc, oid, snap, gid, mode, spiegel)
|
||
|
||
|
||
func _test_build_place_point() -> void:
|
||
print("Testgruppe: Bauteil setzen → Zwilling (Punktspiegelung)")
|
||
var doc := _new_doc()
|
||
var gid := BauOps.gruppe_neu(doc)
|
||
var r := _setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), POINT, gid, gid + 1)
|
||
var objs: Array = doc["objects"]
|
||
_check(objs.size() == 2 and int(r["twin"]) == 1, "Original + Zwilling im Objekt-Block")
|
||
var orig: Dictionary = objs[0]
|
||
var twin: Dictionary = objs[1]
|
||
_check(str(twin["id"]) == "bau_wand", "Zwilling hat gleiche ID")
|
||
var op := _pos3(orig)
|
||
var tp := _pos3(twin)
|
||
_check(_near(tp.x, -op.x) and _near(tp.z, -op.z) and _near(tp.y, op.y), "Zwilling-Position punktgespiegelt")
|
||
_check(_near(fposmod(float(twin["rot_y_deg"]) - float(orig["rot_y_deg"]), 360.0), 180.0), "Zwilling um 180° gedreht")
|
||
var key := SymmetryCore.sym_of(orig)
|
||
_check(not key.is_empty() and SymmetryCore.sym_of(twin) == key, "gleiche sym-Kennung")
|
||
_check(GroupOps.group_id(orig) == gid and GroupOps.group_id(twin) == gid + 1, "Zwilling bekommt das Spiegel-Gebäude")
|
||
|
||
|
||
func _test_build_place_axes() -> void:
|
||
print("Testgruppe: Bauteil setzen → Zwilling (Achsen X / Z)")
|
||
for mode in [AXIS_X, AXIS_Z]:
|
||
var doc := _new_doc()
|
||
var gid := BauOps.gruppe_neu(doc)
|
||
_setze(doc, "bau_fundament", Vector3(11.0, 0.0, 21.0), mode, gid, gid + 1)
|
||
var objs: Array = doc["objects"]
|
||
_check(objs.size() == 2, "Modus %d: Zwilling da" % mode)
|
||
var op := _pos3(objs[0])
|
||
var tp := _pos3(objs[1])
|
||
if mode == AXIS_X:
|
||
_check(_near(tp.x, -op.x) and _near(tp.z, op.z), "Achse X: x gespiegelt, z gleich")
|
||
else:
|
||
_check(_near(tp.z, -op.z) and _near(tp.x, op.x), "Achse Z: z gespiegelt, x gleich")
|
||
|
||
|
||
func _test_build_replace_follows() -> void:
|
||
print("Testgruppe: Tür einsetzen → Zwilling bekommt auch die Tür")
|
||
var doc := _new_doc()
|
||
var gid := BauOps.gruppe_neu(doc)
|
||
_setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), POINT, gid, gid + 1)
|
||
var r := _setze(doc, "bau_wand_tuer", Vector3(11.0, 1.5, 20.1), POINT, gid, gid + 1)
|
||
var objs: Array = doc["objects"]
|
||
_check(bool(r["ersetzt"]) and objs.size() == 2, "ersetzt statt anhängen")
|
||
_check(str(objs[0]["id"]) == "bau_wand_tuer" and str(objs[1]["id"]) == "bau_wand_tuer", "Original UND Zwilling haben die Tür")
|
||
|
||
|
||
func _test_build_off_gegenprobe() -> void:
|
||
print("Testgruppe: Symmetrie AUS = ein Teil, kein Zwilling")
|
||
var doc := _new_doc()
|
||
var gid := BauOps.gruppe_neu(doc)
|
||
var r := _setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), OFF, gid)
|
||
_check((doc["objects"] as Array).size() == 1 and int(r["twin"]) == -1, "kein Zwilling im Modus AUS")
|
||
_check(SymmetryCore.sym_of((doc["objects"] as Array)[0]).is_empty(), "keine sym-Kennung")
|
||
|
||
|
||
func _test_build_one_undo_step() -> void:
|
||
print("Testgruppe: EIN Undo-Schritt nimmt Original + Zwilling zurück")
|
||
var doc := _new_doc()
|
||
var hist := ActionHistory.new()
|
||
hist.begin(doc)
|
||
var before := hist.snapshot(["objects", "group_names"])
|
||
if not before.has("group_names"):
|
||
before["group_names"] = null
|
||
var gid := BauOps.gruppe_neu(doc)
|
||
_setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), POINT, gid, gid + 1)
|
||
hist.commit("Wand bauen", before, hist.snapshot(["objects", "group_names"]))
|
||
_check((doc["objects"] as Array).size() == 2, "nach dem Setzen: 2 Teile")
|
||
hist.undo()
|
||
_check((doc["objects"] as Array).is_empty(), "EIN Undo: beide weg")
|
||
hist.redo()
|
||
_check((doc["objects"] as Array).size() == 2, "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)
|