t_ffa207b1: M4 Bau-Teile + Spawnpunkte spiegeln (Spec vc27)
- 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).
This commit is contained in:
parent
0adb161386
commit
c15c606311
7 changed files with 860 additions and 9 deletions
|
|
@ -350,3 +350,105 @@ static func _ray_tri(origin: Vector3, dir: Vector3, a: Vector3, b: Vector3, c: V
|
||||||
if v < 0.0 or u + v > 1.0:
|
if v < 0.0 or u + v > 1.0:
|
||||||
return -1.0
|
return -1.0
|
||||||
return e2.dot(q) * inv
|
return e2.dot(q) * inv
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ Symmetrie (M4) ------
|
||||||
|
## Karte t_ffa207b1 (Spec /opt/data/scopedvr/KARTEN_MIRROR_vc27.md): Im
|
||||||
|
## Symmetrie-Modus bekommt jedes gesetzte Bau-Teil automatisch einen Zwilling
|
||||||
|
## auf der anderen Kartenseite — gleiche Snap-Regeln (Position wird 1:1
|
||||||
|
## gespiegelt, Parität „ecke"/„mitte" bleibt bei zentrierter Karte erhalten),
|
||||||
|
## gleiche kind_id, gleiche Punkte/Details. Gekoppelt über das `sym`-Feld aus
|
||||||
|
## M1 (SymmetryCore). ALLE Funktionen sind pure Statics → headless testbar.
|
||||||
|
## Aufrufer ist build_tool.gd (dort leben die Klick-Mutationen); die Historie
|
||||||
|
## snapshotet den Block „gebaeude" um den Aufruf herum → EIN Undo-Schritt
|
||||||
|
## nimmt Original UND Zwilling zurück.
|
||||||
|
|
||||||
|
## V9-Grenzen (M4): dieselbe Regel wie der Loader-Kern (svmap_io.gd,
|
||||||
|
## _inside_bounds mit ±1 m Toleranz). true = gespiegeltes Teil darf gespeichert
|
||||||
|
## werden; false → Aufrufer meldet V9 im Status statt still zu speichern.
|
||||||
|
static func inside_match_bounds(pos: Vector3, doc: Dictionary) -> bool:
|
||||||
|
var bounds: Dictionary = doc.get("bounds", {})
|
||||||
|
if bounds.is_empty():
|
||||||
|
return true
|
||||||
|
return SvmapIO._inside_bounds([pos.x, pos.y, pos.z], bounds)
|
||||||
|
|
||||||
|
|
||||||
|
## Zwilling eines frisch gesetzten Bauteils anlegen (Symmetrie an). `teil` muss
|
||||||
|
## bereits im doc hängen (der sym-Link wird auf beide Einträge gesetzt).
|
||||||
|
## Rückgabe: Klartext-Zusatz für die Statuszeile ("" = still gespiegelt).
|
||||||
|
static func place_mirrored(doc: Dictionary, teil: Dictionary, mode: int) -> String:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return ""
|
||||||
|
var label := kind_label(str(teil.get("id", "")))
|
||||||
|
var pos := _pos_of(teil)
|
||||||
|
var center := SymmetryCore.map_center(doc)
|
||||||
|
if SymmetryCore.maps_to_self(mode, pos, center):
|
||||||
|
return "%s liegt auf der Symmetrie-Mitte — nicht gedoppelt." % label
|
||||||
|
var twin := teil.duplicate(true) # tief: punkte/details dürfen nicht aliasen
|
||||||
|
var mirror_pos := SymmetryCore.mirror_point(mode, pos, center)
|
||||||
|
twin["pos"] = [mirror_pos.x, mirror_pos.y, mirror_pos.z]
|
||||||
|
twin["rot"] = SymmetryCore.mirror_rot_y(mode, float(teil.get("rot", 0.0)))
|
||||||
|
if not inside_match_bounds(mirror_pos, doc):
|
||||||
|
return "❌ V9: Gespiegeltes %s läge außerhalb der Match-Grenzen — nur das Original wurde gesetzt." % label
|
||||||
|
var key := SymmetryCore.next_sym_key(doc)
|
||||||
|
SymmetryCore.set_sym(teil, key)
|
||||||
|
SymmetryCore.set_sym(twin, key)
|
||||||
|
ensure_block(doc).append(twin)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Zwilling nach Drehen/Anheben/Verschieben/Auf-Boden neu ausrichten: er bekommt
|
||||||
|
## immer die gespiegelte Form der NEUEN Transform des Originals (Drehung über
|
||||||
|
## mirror_rot_y, Position über mirror_point, y bleibt Skalar). Teile ohne
|
||||||
|
## sym-Partner (z. B. bei ausgeschalteter Symmetrie gebaut) bleiben unberührt.
|
||||||
|
## Läge der gespiegelte Zielpunkt außerhalb der V9-Grenzen, bleibt der Zwilling
|
||||||
|
## UNVERÄNDERT und die Meldung geht in den Status. Rückgabe: Klartext ("" = ok).
|
||||||
|
static func update_mirrored(doc: Dictionary, index: int, mode: int) -> String:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return ""
|
||||||
|
var list := pieces(doc)
|
||||||
|
if index < 0 or index >= list.size() or not (list[index] is Dictionary):
|
||||||
|
return ""
|
||||||
|
var teil := list[index] as Dictionary
|
||||||
|
var key := SymmetryCore.sym_of(teil)
|
||||||
|
if key.is_empty():
|
||||||
|
return ""
|
||||||
|
var partner_idx := SymmetryCore.find_partner_index(list, key, index)
|
||||||
|
if partner_idx < 0 or not (list[partner_idx] is Dictionary):
|
||||||
|
return ""
|
||||||
|
var mirror_pos := SymmetryCore.mirror_point(mode, _pos_of(teil), SymmetryCore.map_center(doc))
|
||||||
|
if not inside_match_bounds(mirror_pos, doc):
|
||||||
|
return "❌ V9: Gespiegeltes Bauteil läge außerhalb der Match-Grenzen — Zwilling nicht verschoben."
|
||||||
|
var twin := list[partner_idx] as Dictionary
|
||||||
|
twin["pos"] = [mirror_pos.x, mirror_pos.y, mirror_pos.z]
|
||||||
|
twin["rot"] = SymmetryCore.mirror_rot_y(mode, float(teil.get("rot", 0.0)))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Bauteil löschen — bei eingeschalteter Symmetrie den sym-Partner mit
|
||||||
|
## (entfernen → beide weg). Index-Verschiebung sicher: der HÖHERE Index wird
|
||||||
|
## zuerst entfernt. Modus AUS = exakt das alte Verhalten (nur ein Teil weg).
|
||||||
|
## Rückgabe: Klartext-Status ("" = nur das eine Teil entfernt).
|
||||||
|
static func remove_mirrored(doc: Dictionary, index: int, mode: int) -> String:
|
||||||
|
var list := pieces(doc)
|
||||||
|
if index < 0 or index >= list.size() or not (list[index] is Dictionary):
|
||||||
|
return ""
|
||||||
|
var teil := list[index] as Dictionary
|
||||||
|
var partner_idx := -1
|
||||||
|
if mode != SymmetryCore.Mode.OFF:
|
||||||
|
partner_idx = SymmetryCore.find_partner_index(list, SymmetryCore.sym_of(teil), index)
|
||||||
|
if partner_idx < 0:
|
||||||
|
list.remove_at(index)
|
||||||
|
return ""
|
||||||
|
if partner_idx > index:
|
||||||
|
list.remove_at(partner_idx)
|
||||||
|
list.remove_at(index)
|
||||||
|
else:
|
||||||
|
list.remove_at(index)
|
||||||
|
list.remove_at(partner_idx)
|
||||||
|
return "Der gespiegelte Zwilling wurde mit entfernt."
|
||||||
|
|
||||||
|
|
||||||
|
static func _pos_of(teil: Dictionary) -> Vector3:
|
||||||
|
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
return Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||||||
|
|
|
||||||
|
|
@ -200,10 +200,14 @@ func _place_piece() -> void:
|
||||||
return
|
return
|
||||||
BuildOps.ensure_block(main.doc).append(teil)
|
BuildOps.ensure_block(main.doc).append(teil)
|
||||||
var label := BuildOps.kind_label(kind_id)
|
var label := BuildOps.kind_label(kind_id)
|
||||||
|
# Symmetrie (M4, t_ffa207b1): Zwilling anlegen — innerhalb desselben
|
||||||
|
# Undo-Schritts (Snapshot läuft um beide Mutationen herum).
|
||||||
|
var mirror_msg: String = BuildOps.place_mirrored(main.doc, teil, _sym_mode())
|
||||||
main.history.commit("%s gesetzt" % label, before, main.history.snapshot(["gebaeude"]))
|
main.history.commit("%s gesetzt" % label, before, main.history.snapshot(["gebaeude"]))
|
||||||
_after_build_change()
|
_after_build_change()
|
||||||
main.call("_set_status", "%s gesetzt (%d Bauteile). Weiter klicken zum Bauen — R dreht, Strg+Z nimmt das letzte Teil zurück." % [
|
main.call("_set_status", "%s gesetzt (%d Bauteile).%s Weiter klicken zum Bauen — R dreht, Strg+Z nimmt das letzte Teil zurück." % [
|
||||||
label, BuildOps.piece_count(main.doc)])
|
label, BuildOps.piece_count(main.doc),
|
||||||
|
"" if mirror_msg.is_empty() else " " + mirror_msg])
|
||||||
|
|
||||||
|
|
||||||
func _select_at(viewport_pos: Vector2) -> void:
|
func _select_at(viewport_pos: Vector2) -> void:
|
||||||
|
|
@ -238,6 +242,8 @@ func rotate_selected() -> void:
|
||||||
var teil: Dictionary = list[selected_index]
|
var teil: Dictionary = list[selected_index]
|
||||||
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
||||||
teil["rot"] = wrapf(float(teil.get("rot", 0.0)) + 90.0, 0.0, 360.0)
|
teil["rot"] = wrapf(float(teil.get("rot", 0.0)) + 90.0, 0.0, 360.0)
|
||||||
|
# Symmetrie (M4): Zwilling bekommt die gespiegelte neue Drehung.
|
||||||
|
BuildOps.update_mirrored(main.doc, selected_index, _sym_mode())
|
||||||
main.history.commit("%s gedreht (90°)" % _piece_label(selected_index),
|
main.history.commit("%s gedreht (90°)" % _piece_label(selected_index),
|
||||||
before, main.history.snapshot(["gebaeude"]), "bauen-rot:%d" % selected_index)
|
before, main.history.snapshot(["gebaeude"]), "bauen-rot:%d" % selected_index)
|
||||||
_after_build_change()
|
_after_build_change()
|
||||||
|
|
@ -255,6 +261,8 @@ func lift_selected(delta_m: float) -> void:
|
||||||
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
|
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
|
||||||
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
||||||
teil["pos"] = [float(pos[0]), BuildOps.snap_y(float(pos[1]) + delta_m), float(pos[2])]
|
teil["pos"] = [float(pos[0]), BuildOps.snap_y(float(pos[1]) + delta_m), float(pos[2])]
|
||||||
|
# Symmetrie (M4): Zwilling heben/absenken mit (y bleibt gespiegelt identisch).
|
||||||
|
BuildOps.update_mirrored(main.doc, selected_index, _sym_mode())
|
||||||
main.history.commit("%s %s" % [_piece_label(selected_index),
|
main.history.commit("%s %s" % [_piece_label(selected_index),
|
||||||
"angehoben" if delta_m > 0.0 else "abgesenkt"],
|
"angehoben" if delta_m > 0.0 else "abgesenkt"],
|
||||||
before, main.history.snapshot(["gebaeude"]), "bauen-lift:%d" % selected_index)
|
before, main.history.snapshot(["gebaeude"]), "bauen-lift:%d" % selected_index)
|
||||||
|
|
@ -280,6 +288,8 @@ func nudge_selected(delta: Vector3) -> void:
|
||||||
var nx := float(pos[0]) + delta.x
|
var nx := float(pos[0]) + delta.x
|
||||||
var nz := float(pos[2]) + delta.z
|
var nz := float(pos[2]) + delta.z
|
||||||
teil["pos"] = [nx, float(pos[1]), nz]
|
teil["pos"] = [nx, float(pos[1]), nz]
|
||||||
|
# Symmetrie (M4): Zwilling wandert gespiegelt mit (Raster-Parität bleibt).
|
||||||
|
BuildOps.update_mirrored(main.doc, selected_index, _sym_mode())
|
||||||
main.history.commit("%s verschoben (Pfeiltaste)" % _piece_label(selected_index),
|
main.history.commit("%s verschoben (Pfeiltaste)" % _piece_label(selected_index),
|
||||||
before, main.history.snapshot(["gebaeude"]))
|
before, main.history.snapshot(["gebaeude"]))
|
||||||
_after_build_change()
|
_after_build_change()
|
||||||
|
|
@ -306,6 +316,8 @@ func snap_selected_to_floor() -> void:
|
||||||
return
|
return
|
||||||
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
||||||
teil["pos"] = [float(pos[0]), target, float(pos[2])]
|
teil["pos"] = [float(pos[0]), target, float(pos[2])]
|
||||||
|
# Symmetrie (M4): Zwilling mit absenken (y bleibt gespiegelt identisch).
|
||||||
|
BuildOps.update_mirrored(main.doc, selected_index, _sym_mode())
|
||||||
main.history.commit("%s auf den Boden gesetzt" % _piece_label(selected_index),
|
main.history.commit("%s auf den Boden gesetzt" % _piece_label(selected_index),
|
||||||
before, main.history.snapshot(["gebaeude"]))
|
before, main.history.snapshot(["gebaeude"]))
|
||||||
_after_build_change()
|
_after_build_change()
|
||||||
|
|
@ -323,11 +335,14 @@ func delete_selected() -> void:
|
||||||
return
|
return
|
||||||
var label := _piece_label(selected_index)
|
var label := _piece_label(selected_index)
|
||||||
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
||||||
list.remove_at(selected_index)
|
# Symmetrie (M4): bei eingeschalteter Symmetrie den sym-Zwilling mit löschen.
|
||||||
|
var mirror_msg: String = BuildOps.remove_mirrored(main.doc, selected_index, _sym_mode())
|
||||||
main.history.commit("%s gelöscht" % label, before, main.history.snapshot(["gebaeude"]))
|
main.history.commit("%s gelöscht" % label, before, main.history.snapshot(["gebaeude"]))
|
||||||
selected_index = -1
|
selected_index = -1
|
||||||
_after_build_change()
|
_after_build_change()
|
||||||
main.call("_set_status", "%s — Strg+Z holt es zurück." % label.replace(" (Teil", " gelöscht (Teil"))
|
main.call("_set_status", "%s — Strg+Z holt es zurück.%s" % [
|
||||||
|
label.replace(" (Teil", " gelöscht (Teil"),
|
||||||
|
"" if mirror_msg.is_empty() else " " + mirror_msg])
|
||||||
|
|
||||||
|
|
||||||
func _after_build_change() -> void:
|
func _after_build_change() -> void:
|
||||||
|
|
@ -365,11 +380,21 @@ func demo_place(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> Dictionar
|
||||||
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
var before: Dictionary = main.history.snapshot(["gebaeude"])
|
||||||
var teil := BuildOps.make_piece(p_kind, pos, rot_deg)
|
var teil := BuildOps.make_piece(p_kind, pos, rot_deg)
|
||||||
BuildOps.ensure_block(main.doc).append(teil)
|
BuildOps.ensure_block(main.doc).append(teil)
|
||||||
|
# Symmetrie (M4): Zwilling im selben Undo-Schritt (wie beim Klick).
|
||||||
|
BuildOps.place_mirrored(main.doc, teil, _sym_mode())
|
||||||
main.history.commit("%s gesetzt" % BuildOps.kind_label(p_kind),
|
main.history.commit("%s gesetzt" % BuildOps.kind_label(p_kind),
|
||||||
before, main.history.snapshot(["gebaeude"]))
|
before, main.history.snapshot(["gebaeude"]))
|
||||||
return teil
|
return teil
|
||||||
|
|
||||||
|
|
||||||
|
## Aktiver Symmetrie-Modus aus main (M1-Schalter). Defensive über main.get():
|
||||||
|
## Mock-Mains (Tests) haben sym_mode nicht — dann gilt OFF (altes Verhalten),
|
||||||
|
## statt mit SCRIPT ERROR abzustürzen (Lektion: SCRIPT ERROR in -s-Suiten = rot).
|
||||||
|
func _sym_mode() -> int:
|
||||||
|
var v: Variant = main.get("sym_mode") if main != null else null
|
||||||
|
return SymmetryCore.Mode.OFF if v == null else int(v)
|
||||||
|
|
||||||
|
|
||||||
## Demo-Hook: Ghost an Position parken (für Screenshots ohne Maus).
|
## Demo-Hook: Ghost an Position parken (für Screenshots ohne Maus).
|
||||||
func demo_park_ghost(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> void:
|
func demo_park_ghost(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> void:
|
||||||
set_kind(p_kind)
|
set_kind(p_kind)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ extends Node3D
|
||||||
|
|
||||||
const MODE_HELP := "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nRadius/Team rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
const MODE_HELP := "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nRadius/Team rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
||||||
|
|
||||||
|
## Symmetrie (M4, Karte t_ffa207b1): Partner-Kopplung gespiegelter Spawn-Zonen.
|
||||||
|
## Liegt in design_notes (Loader-frei, rundtrip-sicher — wie spawn_radius_m):
|
||||||
|
## {"A5": "B3", "B3": "A5"} — direkte Partner-IDs, beide Richtungen gesetzt.
|
||||||
|
const SPAWN_MIRROR_KEY := "spawn_mirror"
|
||||||
|
|
||||||
var main # Hauptfenster (script-erweitert, Variant)
|
var main # Hauptfenster (script-erweitert, Variant)
|
||||||
var camera: Camera3D
|
var camera: Camera3D
|
||||||
var view_builder # Welt-Bauer (script-erweitert, Variant)
|
var view_builder # Welt-Bauer (script-erweitert, Variant)
|
||||||
|
|
@ -265,12 +270,15 @@ func _on_left_release() -> void:
|
||||||
pos.z = clampf(pos.z, -half.y, half.y)
|
pos.z = clampf(pos.z, -half.y, half.y)
|
||||||
var before: Dictionary = SpawnOps.snapshot_blocks(main.history, main.doc)
|
var before: Dictionary = SpawnOps.snapshot_blocks(main.history, main.doc)
|
||||||
SpawnOps.move_point(main.doc, selected_team, selected_id, pos.x, pos.z)
|
SpawnOps.move_point(main.doc, selected_team, selected_id, pos.x, pos.z)
|
||||||
|
# Symmetrie (M4): Zwilling folgt der gespiegelten neuen Position.
|
||||||
|
var mirror_msg: String = spawn_mirror_move(main.doc, selected_team, selected_id, _sym_mode())
|
||||||
main.history.commit("%s verschoben" % selected_id,
|
main.history.commit("%s verschoben" % selected_id,
|
||||||
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
||||||
main.call("_after_structure_change")
|
main.call("_after_structure_change")
|
||||||
_point_selection_at_doc()
|
_point_selection_at_doc()
|
||||||
view_builder.call("set_spawn_selected", selected_team, selected_id)
|
view_builder.call("set_spawn_selected", selected_team, selected_id)
|
||||||
main.call("_set_status", "%s verschoben — Strg+Z macht das rückgängig." % selected_id)
|
main.call("_set_status", "%s verschoben — Strg+Z macht das rückgängig.%s" % [
|
||||||
|
selected_id, "" if mirror_msg.is_empty() else " " + mirror_msg])
|
||||||
_drag_moved = false
|
_drag_moved = false
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -289,6 +297,8 @@ func _place_zone(viewport_pos: Vector2) -> void:
|
||||||
var point := SpawnOps.add_zone(main.doc, team, pos.x, pos.z)
|
var point := SpawnOps.add_zone(main.doc, team, pos.x, pos.z)
|
||||||
var pid := str(point["id"])
|
var pid := str(point["id"])
|
||||||
SpawnOps.set_radius(main.doc, pid, zone_radius)
|
SpawnOps.set_radius(main.doc, pid, zone_radius)
|
||||||
|
# Symmetrie (M4): Zwilling im Gegner-Team — innerhalb desselben Undo-Schritts.
|
||||||
|
var mirror_res: Dictionary = spawn_mirror_add(main.doc, team, pid, _sym_mode())
|
||||||
main.history.commit("Spawn-Zone %s (%s) gesetzt" % [pid, SpawnOps.team_label(team)],
|
main.history.commit("Spawn-Zone %s (%s) gesetzt" % [pid, SpawnOps.team_label(team)],
|
||||||
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
||||||
main.call("_after_structure_change")
|
main.call("_after_structure_change")
|
||||||
|
|
@ -297,8 +307,9 @@ func _place_zone(viewport_pos: Vector2) -> void:
|
||||||
selected_id = pid
|
selected_id = pid
|
||||||
view_builder.call("set_spawn_selected", selected_team, selected_id)
|
view_builder.call("set_spawn_selected", selected_team, selected_id)
|
||||||
refresh_selection_panel()
|
refresh_selection_panel()
|
||||||
main.call("_set_status", "Spawn-Zone %s (%s) gesetzt. Radius: Regler rechts · verschieben: klicken+ziehen · Entf löscht · Strg+Z macht alles rückgängig." % [
|
var mirror_text := str(mirror_res["status"])
|
||||||
pid, SpawnOps.team_label(team)])
|
main.call("_set_status", "Spawn-Zone %s (%s) gesetzt.%s Radius: Regler rechts · verschieben: klicken+ziehen · Entf löscht · Strg+Z macht alles rückgängig." % [
|
||||||
|
pid, SpawnOps.team_label(team), "" if mirror_text.is_empty() else " " + mirror_text])
|
||||||
|
|
||||||
|
|
||||||
## Kursor-Strahl → getroffene Zone ({team, id}) oder {}.
|
## Kursor-Strahl → getroffene Zone ({team, id}) oder {}.
|
||||||
|
|
@ -357,12 +368,15 @@ func delete_selected() -> void:
|
||||||
if not SpawnOps.remove_point(main.doc, selected_team, selected_id):
|
if not SpawnOps.remove_point(main.doc, selected_team, selected_id):
|
||||||
_clear_selection()
|
_clear_selection()
|
||||||
return
|
return
|
||||||
|
# Symmetrie (M4): Zwilling mit entfernen (innerhalb desselben Undo-Schritts).
|
||||||
|
var mirror_msg: String = spawn_mirror_delete(main.doc, selected_id, _sym_mode())
|
||||||
main.history.commit("%s löschen" % label, before,
|
main.history.commit("%s löschen" % label, before,
|
||||||
SpawnOps.snapshot_blocks(main.history, main.doc))
|
SpawnOps.snapshot_blocks(main.history, main.doc))
|
||||||
_clear_selection()
|
_clear_selection()
|
||||||
main.call("_after_structure_change")
|
main.call("_after_structure_change")
|
||||||
main.call("_set_status", "Spawn-Zone %s gelöscht — Strg+Z holt sie zurück. %s braucht mindestens %d Zonen!" % [
|
main.call("_set_status", "Spawn-Zone %s gelöscht — Strg+Z holt sie zurück.%s %s braucht mindestens %d Zonen!" % [
|
||||||
label, SpawnOps.team_label(team), SvmapIO.MIN_SPAWN_POINTS_PER_TEAM])
|
label, "" if mirror_msg.is_empty() else " " + mirror_msg,
|
||||||
|
SpawnOps.team_label(team), SvmapIO.MIN_SPAWN_POINTS_PER_TEAM])
|
||||||
|
|
||||||
|
|
||||||
## Panel-Zeile rechts (Welche Zone ist an?) synchron halten — ohne Statuszeile anzufassen.
|
## Panel-Zeile rechts (Welche Zone ist an?) synchron halten — ohne Statuszeile anzufassen.
|
||||||
|
|
@ -371,6 +385,138 @@ func refresh_selection_panel() -> void:
|
||||||
main.call("_sync_spawn_selection")
|
main.call("_sync_spawn_selection")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------- Symmetrie (M4) Statics ---
|
||||||
|
## Karte t_ffa207b1: Gespiegelte Spawn-Zonen. ENTScheidung (Spec vc27,
|
||||||
|
## „Teams bleiben einander gegenüber fair"): der Zwilling landet im GEGNER-
|
||||||
|
## Team — Blau (West) ↔ Rot (Ost), denn genau auf den Gegenseiten liegen die
|
||||||
|
## Default-Streifen der beiden Teams. Jede gesetzte Zone gibt dem anderen Team
|
||||||
|
## automatisch die faire Gegenstelle. Alles pure Statics (kein
|
||||||
|
## UI, kein Node-Zugriff) → headless testbar; spawn_ops.gd bleibt unangetastet.
|
||||||
|
|
||||||
|
## Partner-ID einer Spawn-Zone ("" = keiner).
|
||||||
|
static func spawn_mirror_partner(doc: Dictionary, pid: String) -> String:
|
||||||
|
var m := _mirror_map_of(doc)
|
||||||
|
return str(m.get(pid, ""))
|
||||||
|
|
||||||
|
|
||||||
|
static func _mirror_map_of(doc: Dictionary) -> Dictionary:
|
||||||
|
var notes = doc.get("design_notes", {})
|
||||||
|
if not (notes is Dictionary):
|
||||||
|
return {}
|
||||||
|
var m = (notes as Dictionary).get(SPAWN_MIRROR_KEY, {})
|
||||||
|
return m if m is Dictionary else {}
|
||||||
|
|
||||||
|
|
||||||
|
static func _mirror_map_set(doc: Dictionary, pid_a: String, pid_b: String) -> void:
|
||||||
|
var notes = doc.get("design_notes", {})
|
||||||
|
if not (notes is Dictionary):
|
||||||
|
notes = {}
|
||||||
|
doc["design_notes"] = notes
|
||||||
|
var m = (notes as Dictionary).get(SPAWN_MIRROR_KEY, {})
|
||||||
|
if not (m is Dictionary):
|
||||||
|
m = {}
|
||||||
|
(notes as Dictionary)[SPAWN_MIRROR_KEY] = m
|
||||||
|
m[pid_a] = pid_b
|
||||||
|
m[pid_b] = pid_a
|
||||||
|
|
||||||
|
|
||||||
|
static func _mirror_map_clear(doc: Dictionary, pid_a: String, pid_b: String) -> void:
|
||||||
|
var m := _mirror_map_of(doc)
|
||||||
|
if m.is_empty():
|
||||||
|
return
|
||||||
|
m.erase(pid_a)
|
||||||
|
m.erase(pid_b)
|
||||||
|
|
||||||
|
|
||||||
|
## Gespiegelte Spawn-Zone zu `pid` anlegen (Symmetrie an). Rückgabe:
|
||||||
|
## {"ok": bool, "status": Klartext}. ok=false + V9-Text = Zwilling NICHT
|
||||||
|
## gespeichert (Grenzen scharf — Spec vc27). Auf der Symmetrie-Mitte liegende
|
||||||
|
## Zonen werden nicht gedoppelt (Spec).
|
||||||
|
static func spawn_mirror_add(doc: Dictionary, team: int, pid: String, mode: int) -> Dictionary:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return {"ok": true, "status": ""}
|
||||||
|
var original := _spawn_point_of(doc, team, pid)
|
||||||
|
if original.is_empty():
|
||||||
|
return {"ok": false, "status": "Spawn-Zone %s nicht gefunden — Zwilling nicht gesetzt." % pid}
|
||||||
|
var pos := _spawn_pos_of(original)
|
||||||
|
var center := SymmetryCore.map_center(doc)
|
||||||
|
if SymmetryCore.maps_to_self(mode, pos, center):
|
||||||
|
return {"ok": true, "status": "Spawn-Zone %s liegt auf der Symmetrie-Mitte — nicht gedoppelt." % pid}
|
||||||
|
var mirror_pos := SymmetryCore.mirror_point(mode, pos, center)
|
||||||
|
var bounds: Dictionary = doc.get("bounds", {})
|
||||||
|
if not bounds.is_empty() and not SvmapIO._inside_bounds(
|
||||||
|
[mirror_pos.x, mirror_pos.y, mirror_pos.z], bounds):
|
||||||
|
return {"ok": false, "status": "❌ V9: Gespiegelter Spawn läge außerhalb der Match-Grenzen — nicht gesetzt."}
|
||||||
|
var twin := SpawnOps.add_zone(doc, 1 - team, mirror_pos.x, mirror_pos.z)
|
||||||
|
var twin_id := str(twin["id"])
|
||||||
|
SpawnOps.set_radius(doc, twin_id, SpawnOps.zone_radius(doc, pid))
|
||||||
|
_mirror_map_set(doc, pid, twin_id)
|
||||||
|
return {"ok": true, "status": "Gespiegelte Zone %s (%s) gesetzt." % [
|
||||||
|
twin_id, SpawnOps.team_label(1 - team)]}
|
||||||
|
|
||||||
|
|
||||||
|
## Verschiebung spiegeln: Zwilling bekommt die gespiegelte Position der NEUEN
|
||||||
|
## Position des Originals (y = Terrain-Höhe am gespiegelten Punkt, wie beim
|
||||||
|
## normalen Verschieben). Rückgabe: Klartext-Status ("" = nichts zu tun).
|
||||||
|
static func spawn_mirror_move(doc: Dictionary, team: int, pid: String, mode: int) -> String:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return ""
|
||||||
|
var partner := spawn_mirror_partner(doc, pid)
|
||||||
|
if partner.is_empty():
|
||||||
|
return ""
|
||||||
|
var original := _spawn_point_of(doc, team, pid)
|
||||||
|
if original.is_empty():
|
||||||
|
return ""
|
||||||
|
var pos := _spawn_pos_of(original)
|
||||||
|
var center := SymmetryCore.map_center(doc)
|
||||||
|
if SymmetryCore.maps_to_self(mode, pos, center):
|
||||||
|
return ""
|
||||||
|
var mirror_pos := SymmetryCore.mirror_point(mode, pos, center)
|
||||||
|
var partner_team := _team_of_point(doc, partner)
|
||||||
|
if partner_team < 0:
|
||||||
|
return ""
|
||||||
|
if not SpawnOps.move_point(doc, partner_team, partner, mirror_pos.x, mirror_pos.z):
|
||||||
|
return ""
|
||||||
|
return "Gespiegelte Zone %s mitverschoben." % partner
|
||||||
|
|
||||||
|
|
||||||
|
## Löschung spiegeln (entfernen → beide weg, inkl. Radius-Eintrag und
|
||||||
|
## Partner-Map). Rückgabe: Klartext-Status ("" = nur das Original war weg).
|
||||||
|
static func spawn_mirror_delete(doc: Dictionary, pid: String, mode: int) -> String:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return ""
|
||||||
|
var partner := spawn_mirror_partner(doc, pid)
|
||||||
|
if partner.is_empty():
|
||||||
|
return ""
|
||||||
|
var partner_team := _team_of_point(doc, partner)
|
||||||
|
if partner_team < 0:
|
||||||
|
_mirror_map_clear(doc, pid, partner)
|
||||||
|
return ""
|
||||||
|
if SpawnOps.remove_point(doc, partner_team, partner):
|
||||||
|
_mirror_map_clear(doc, pid, partner)
|
||||||
|
return "Gespiegelte Zone %s (%s) mit entfernt." % [partner, SpawnOps.team_label(partner_team)]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
static func _spawn_point_of(doc: Dictionary, team: int, pid: String) -> Dictionary:
|
||||||
|
for p in SpawnOps.points(doc, team):
|
||||||
|
if p is Dictionary and str(p.get("id", "")) == pid:
|
||||||
|
return p as Dictionary
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
static func _team_of_point(doc: Dictionary, pid: String) -> int:
|
||||||
|
for team in [0, 1]:
|
||||||
|
if not _spawn_point_of(doc, team, pid).is_empty():
|
||||||
|
return team
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
static func _spawn_pos_of(point: Dictionary) -> Vector3:
|
||||||
|
var pos: Array = point.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
return Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ Ghost-Optik ---
|
# ------------------------------------------------------------ Ghost-Optik ---
|
||||||
|
|
||||||
func _refresh_ghost_look() -> void:
|
func _refresh_ghost_look() -> void:
|
||||||
|
|
@ -417,6 +563,8 @@ func demo_place(zone_team: int, x: float, z: float, radius: float) -> String:
|
||||||
var pid := str(point["id"])
|
var pid := str(point["id"])
|
||||||
if radius > 0.0:
|
if radius > 0.0:
|
||||||
SpawnOps.set_radius(main.doc, pid, radius)
|
SpawnOps.set_radius(main.doc, pid, radius)
|
||||||
|
# Symmetrie (M4): Zwilling im Gegner-Team — innerhalb desselben Undo-Schritts.
|
||||||
|
spawn_mirror_add(main.doc, zone_team, pid, _sym_mode())
|
||||||
main.history.commit("Spawn-Zone %s (%s) gesetzt" % [pid, SpawnOps.team_label(zone_team)],
|
main.history.commit("Spawn-Zone %s (%s) gesetzt" % [pid, SpawnOps.team_label(zone_team)],
|
||||||
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
||||||
return pid
|
return pid
|
||||||
|
|
@ -426,10 +574,20 @@ func demo_place(zone_team: int, x: float, z: float, radius: float) -> String:
|
||||||
func demo_move(zone_team: int, pid: String, x: float, z: float) -> void:
|
func demo_move(zone_team: int, pid: String, x: float, z: float) -> void:
|
||||||
var before: Dictionary = SpawnOps.snapshot_blocks(main.history, main.doc)
|
var before: Dictionary = SpawnOps.snapshot_blocks(main.history, main.doc)
|
||||||
SpawnOps.move_point(main.doc, zone_team, pid, x, z)
|
SpawnOps.move_point(main.doc, zone_team, pid, x, z)
|
||||||
|
# Symmetrie (M4): Zwilling folgt der gespiegelten neuen Position.
|
||||||
|
spawn_mirror_move(main.doc, zone_team, pid, _sym_mode())
|
||||||
main.history.commit("%s verschoben" % pid,
|
main.history.commit("%s verschoben" % pid,
|
||||||
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
||||||
|
|
||||||
|
|
||||||
|
## Aktiver Symmetrie-Modus aus main (M1-Schalter). Defensive über main.get():
|
||||||
|
## Mock-Mains (Tests) haben sym_mode nicht — dann gilt OFF (altes Verhalten),
|
||||||
|
## statt mit SCRIPT ERROR abzustürzen (Lektion: SCRIPT ERROR in -s-Suiten = rot).
|
||||||
|
func _sym_mode() -> int:
|
||||||
|
var v: Variant = main.get("sym_mode") if main != null else null
|
||||||
|
return SymmetryCore.Mode.OFF if v == null else int(v)
|
||||||
|
|
||||||
|
|
||||||
## Demo-Hook: Ghost an Position parken (für Screenshots ohne Maus).
|
## Demo-Hook: Ghost an Position parken (für Screenshots ohne Maus).
|
||||||
func demo_park_ghost(x: float, z: float, team_i: int, radius: float) -> void:
|
func demo_park_ghost(x: float, z: float, team_i: int, radius: float) -> void:
|
||||||
set_team(team_i)
|
set_team(team_i)
|
||||||
|
|
|
||||||
124
tests/probe_m4_mirror_render.gd
Normal file
124
tests/probe_m4_mirror_render.gd
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
extends SceneTree
|
||||||
|
## M4-Beweis (t_ffa207b1): Bau-Teile + Spawn-Zonen werden im Symmetrie-Modus
|
||||||
|
## gespiegelt. Ein Blick von oben auf eine 256-m-Karte: links Original
|
||||||
|
## (Fundament + Wand, blaue Zone), rechts der Zwilling (gleiche kind_id,
|
||||||
|
## gespiegelte Drehung; rote Zone im Gegner-Team).
|
||||||
|
##
|
||||||
|
## xvfb-run -a /opt/data/home/bin/godot4 --rendering-driver opengl3 --path . \
|
||||||
|
## --resolution 1600x900 -s res://tests/probe_m4_mirror_render.gd
|
||||||
|
##
|
||||||
|
## Ausgabe: /tmp/m4_mirror_beweis.png + Prüfzeilen (Zwillinge vorhanden,
|
||||||
|
## Positionen gespiegelt). Nur DER Beweis-Lauf nutzt die echte Code-Kette
|
||||||
|
## (BuildOps.place_mirrored / SpawnTool.spawn_mirror_add) — genau wie die Suite.
|
||||||
|
|
||||||
|
const SpawnTool := preload("res://src/editor/spawn_tool.gd")
|
||||||
|
|
||||||
|
const POINT := SymmetryCore.Mode.POINT
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var manifest: Dictionary = JSON.parse_string(FileAccess.get_file_as_string("res://library/objects.json"))
|
||||||
|
var doc: Dictionary = SvmapDocument.create_new("M4-Symmetrie-Beweis", 256.0, "mittag")
|
||||||
|
|
||||||
|
# --- Bau-Teile: Original West, Zwilling Ost (echte M4-Code-Kette) --------
|
||||||
|
var fund := BuildOps.make_piece("fundament", Vector3(-21.0, 0.0, 3.0), 0.0)
|
||||||
|
BuildOps.ensure_block(doc).append(fund)
|
||||||
|
var fund_msg: String = BuildOps.place_mirrored(doc, fund, POINT)
|
||||||
|
var wand := BuildOps.make_piece("wand", Vector3(-20.0, 0.2, -2.0), 90.0)
|
||||||
|
BuildOps.ensure_block(doc).append(wand)
|
||||||
|
var wand_msg: String = BuildOps.place_mirrored(doc, wand, POINT)
|
||||||
|
|
||||||
|
# --- Spawn: blaue Zone West → rote Zwilling-Zone Ost ---------------------
|
||||||
|
var point := SpawnOps.add_zone(doc, 0, -100.0, 30.0)
|
||||||
|
var spawn_res: Dictionary = SpawnTool.spawn_mirror_add(doc, 0, str(point["id"]), POINT)
|
||||||
|
|
||||||
|
# --- Prüfzeilen (echte Werte, keine Behauptungen) -------------------------
|
||||||
|
var fehler: Array = []
|
||||||
|
_check(BuildOps.piece_count(doc) == 4, "4 Bauteile (2 Paare)", fehler)
|
||||||
|
_check(fund_msg == "" and wand_msg == "", "beide Bau-Teile still gespiegelt", fehler)
|
||||||
|
var twin_fund := _piece_at_pos(doc, 21.0)
|
||||||
|
_check(not twin_fund.is_empty(), "Fundament-Zwilling bei +21", fehler)
|
||||||
|
var twin_wand := _wand_twin(doc)
|
||||||
|
_check(not twin_wand.is_empty(), "Wand-Zwilling vorhanden", fehler)
|
||||||
|
if not twin_wand.is_empty():
|
||||||
|
var rot: float = float(twin_wand.get("rot", 0.0))
|
||||||
|
_check(SymmetryCore.sym_of(wand) == SymmetryCore.sym_of(twin_wand), "Wand-Paar via sym gekoppelt", fehler)
|
||||||
|
print("WAND-ZWILLING: pos=%.1f/%.1f rot=%.0f (Original 90° → gespiegelt %.0f)" % [
|
||||||
|
(float(twin_wand["pos"][0])), float(twin_wand["pos"][2]), rot, rot])
|
||||||
|
_check(bool(spawn_res["ok"]), "Spawn gespiegelt: %s" % str(spawn_res["status"]), fehler)
|
||||||
|
_check(SpawnOps.count_points(doc, 1) == 5, "Team Rot: 4 Default + 1 Zwilling", fehler)
|
||||||
|
print("BAU-TEILE: %d | SPAWN-STATUS: %s" % [BuildOps.piece_count(doc), str(spawn_res["status"])])
|
||||||
|
|
||||||
|
# --- Render ---------------------------------------------------------------
|
||||||
|
var bauer: Node3D = load("res://src/viewport/map_view_builder.gd").new()
|
||||||
|
root.add_child(bauer)
|
||||||
|
await process_frame
|
||||||
|
bauer.rebuild(doc, manifest)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# Symmetrie-Achse sichtbar machen (dünne halbtransparente Linie bei x = 0).
|
||||||
|
var achse := MeshInstance3D.new()
|
||||||
|
var achse_mesh := BoxMesh.new()
|
||||||
|
achse_mesh.size = Vector3(0.3, 3.0, 90.0)
|
||||||
|
achse.mesh = achse_mesh
|
||||||
|
var achse_mat := StandardMaterial3D.new()
|
||||||
|
achse_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
achse_mat.albedo_color = Color(1.0, 0.95, 0.2, 0.55)
|
||||||
|
achse_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
achse.material_override = achse_mat
|
||||||
|
achse.position = Vector3(0.0, 1.5, 0.0)
|
||||||
|
root.add_child(achse)
|
||||||
|
|
||||||
|
var cam := Camera3D.new()
|
||||||
|
cam.fov = 55.0
|
||||||
|
root.add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
cam.make_current()
|
||||||
|
|
||||||
|
# Bild 1: TOTAL — gespiegelte Spawn-Zonen (blau links, rot rechts).
|
||||||
|
cam.position = Vector3(0.0, 120.0, 130.0)
|
||||||
|
await process_frame
|
||||||
|
cam.look_at(Vector3(0.0, 0.0, -10.0), Vector3(0.0, 0.0, -1.0))
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
root.get_texture().get_image().save_png("/tmp/m4_beweis_spawns.png")
|
||||||
|
|
||||||
|
# Bild 2: NAH — Bauteil-Paar links/rechts der Achse (beide voll im Rahmen).
|
||||||
|
cam.position = Vector3(0.0, 14.0, 36.0)
|
||||||
|
await process_frame
|
||||||
|
cam.look_at(Vector3(0.0, 0.0, 0.0), Vector3(0.0, 0.0, -1.0))
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
root.get_texture().get_image().save_png("/tmp/m4_beweis_bauteile.png")
|
||||||
|
for f in fehler:
|
||||||
|
print(" FEHLER: %s" % f)
|
||||||
|
print("PROBE_M4_MIRROR_%s" % ("FAIL" if not fehler.is_empty() else "OK"))
|
||||||
|
quit(1 if not fehler.is_empty() else 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(cond: bool, label: String, fehler: Array) -> void:
|
||||||
|
print(" %s: %s" % ["ok" if cond else "FEHLER", label])
|
||||||
|
if not cond:
|
||||||
|
fehler.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _piece_at_pos(doc: Dictionary, x: float) -> Dictionary:
|
||||||
|
for t in BuildOps.pieces(doc):
|
||||||
|
if not (t is Dictionary):
|
||||||
|
continue
|
||||||
|
var pos: Array = (t as Dictionary).get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if absf(float(pos[0]) - x) < 0.01 and str((t as Dictionary).get("id", "")) == "fundament":
|
||||||
|
return t as Dictionary
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _wand_twin(doc: Dictionary) -> Dictionary:
|
||||||
|
for t in BuildOps.pieces(doc):
|
||||||
|
if not (t is Dictionary):
|
||||||
|
continue
|
||||||
|
var pos: Array = (t as Dictionary).get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if str((t as Dictionary).get("id", "")) == "wand" and float(pos[0]) > 0.0:
|
||||||
|
return t as Dictionary
|
||||||
|
return {}
|
||||||
1
tests/probe_m4_mirror_render.gd.uid
Normal file
1
tests/probe_m4_mirror_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://tcxqt666i3bi
|
||||||
440
tests/test_build_spawn_mirror.gd
Normal file
440
tests/test_build_spawn_mirror.gd
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
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)
|
||||||
1
tests/test_build_spawn_mirror.gd.uid
Normal file
1
tests/test_build_spawn_mirror.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bguw2rta4tlam
|
||||||
Loading…
Reference in a new issue