t_f5935023: M1 Symmetrie-Fundament (Spec vc27, Sly 20.10.) — SymmetryCore (Punkt/Achse X/Achse Z, Zell-Abbildung, Partner-Link sym, alle statisch/pure, headless testbar) + Validator V16 in svmap_io (sym muss String, nicht leer, ≤64 Zeichen, genau 2 pro Kennung) + Toolbar-Schalter + Statuszeile-Anzeige in main.gd (Standard=Punkt, Sly sieht Modus immer) + Suite test_symmetry_core (93 Checks: Modus-Katalog, Kartenmitte aus bounds, zweimal-spiegeln=Identity für alle Modi, Achse/Zentrum-nicht-doppeln, Raster-Zell-Mapping inkl. gerader/ungerader res, Partner-Link-API, V16-Validator, Roundtrip mit sym byte-identisch, Legacy-Karte ohne sym unverändert). 17/19 Suiten grün (test_updater 15/75 FAIL + test_glb_view 15/25 FAIL = PRE-EXISTING per git-stash-Verfahren gegen 437f6bf verifiziert, Ursache unabhängig vom Diff). M1 legt nur Mathe + Anzeige fest — gespiegeltes Bauen selbst kommt mit M2 (Terrain), M3 (Objekte), M4 (Bau-Teile + Spawns).
This commit is contained in:
parent
437f6bf368
commit
0adb161386
9 changed files with 576 additions and 0 deletions
189
src/editor/symmetry_core.gd
Normal file
189
src/editor/symmetry_core.gd
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
extends RefCounted
|
||||||
|
class_name SymmetryCore
|
||||||
|
## Symmetrie-Modus im Map Builder (Karte t_f5935023, Spec
|
||||||
|
## /opt/data/scopedvr/KARTEN_MIRROR_vc27.md, Entscheidung Sly 20.10.):
|
||||||
|
## gespiegeltes Bauen — Änderungen an einer Kartenseite landen automatisch
|
||||||
|
## auf der anderen. DIESE Datei ist M1 (Fundament): reine Mathe + Format-
|
||||||
|
## Helfer, KEIN Spiegeln von Aktionen — das kommt mit M2 (Terrain),
|
||||||
|
## M3 (Objekte) und M4 (Bau-Teile + Spawns).
|
||||||
|
##
|
||||||
|
## Spiegel-Mathematik (Spec vc27):
|
||||||
|
## Zentrum = Kartenmitte. Editor-Maps liegen zentriert (bounds
|
||||||
|
## −size/2 … +size/2), die Mitte ist also in Weltkoordinaten (0, 0) —
|
||||||
|
## map_center() rechnet sie trotzdem generell aus den bounds aus
|
||||||
|
## ((min + max) / 2), damit die Helfer auch bei asymmetrischen bounds
|
||||||
|
## korrekt bleiben.
|
||||||
|
## | Modus | Position | rot_y |
|
||||||
|
## |-------------|-----------------------------|--------------|
|
||||||
|
## | Punkt (180°)| x' = 2cx − x, z' = 2cz − z | rot + 180° |
|
||||||
|
## | Achse X | x' = 2cx − x, z' = z | −rot |
|
||||||
|
## | Achse Z | x' = x, z' = 2cz − z | −rot |
|
||||||
|
## y bleibt in ALLEN Modi unverändert (Höhe ist ein Skalar).
|
||||||
|
## Standard = Punktspiegelung (nur sie macht beide Hälften wirklich
|
||||||
|
## deckungsgleich — Achsenspiegelung dreht den Zwilling um 180°, siehe Spec).
|
||||||
|
##
|
||||||
|
## Terrain-Zell-Abbildung (Zelle (i,j), i läuft entlang x, j entlang z —
|
||||||
|
## siehe SvmapDocument.terrain_height_at): Punkt (res−1−i, res−1−j),
|
||||||
|
## Achse X (res−1−i, j), Achse Z (i, res−1−j).
|
||||||
|
##
|
||||||
|
## Partner-Link: ein Paar trägt denselben `sym`-Schlüssel (optionales Feld
|
||||||
|
## an Objekten UND Bau-Teilen). Validator-Regel (svmap_io.gd, V16):
|
||||||
|
## String, nicht leer, ≤ 64 Zeichen, höchstens 2 Objekte pro Kennung.
|
||||||
|
## Alte Karten ohne `sym` laden unverändert (Rückwärtskompatibilität).
|
||||||
|
##
|
||||||
|
## Alles static + reine Funktionen, ohne Node-Abhängigkeit → headless testbar.
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
OFF = 0, # Symmetrie aus
|
||||||
|
POINT = 1, # Punktspiegelung 180° um die Kartenmitte (Standard)
|
||||||
|
AXIS_X = 2, # Achsenspiegelung X (links ↔ rechts)
|
||||||
|
AXIS_Z = 3, # Achsenspiegelung Z (vorn ↔ hinten)
|
||||||
|
}
|
||||||
|
|
||||||
|
## Toolbar-/Statuszeilen-Texte, Index = Mode-Wert.
|
||||||
|
const MODE_LABELS: Array[String] = ["Aus", "Punkt", "Achse X", "Achse Z"]
|
||||||
|
|
||||||
|
## Toleranz in Metern für „liegt auf der Achse/im Zentrum“ — solche Elemente
|
||||||
|
## werden beim gespiegelten Bauen NICHT gedoppelt (Spec vc27).
|
||||||
|
const ON_AXIS_TOLERANCE_M := 0.05
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Modus/Helfer --
|
||||||
|
|
||||||
|
static func mode_label(mode: int) -> String:
|
||||||
|
return MODE_LABELS[clampi(mode, 0, MODE_LABELS.size() - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
## Kartenmitte aus den bounds ((min + max) / 2 in X und Z). Für zentrierte
|
||||||
|
## Editor-Maps = (0, 0), unabhängig von der Kartengröße — die Größe steckt
|
||||||
|
## in den bounds. Fehlen die bounds (defektes doc) → (0, 0).
|
||||||
|
static func map_center(doc: Dictionary) -> Vector2:
|
||||||
|
var bounds: Dictionary = doc.get("bounds", {})
|
||||||
|
var mn: Array = bounds.get("min", [])
|
||||||
|
var mx: Array = bounds.get("max", [])
|
||||||
|
if (mn as Array).size() == 3 and (mx as Array).size() == 3:
|
||||||
|
return Vector2(
|
||||||
|
(float(mn[0]) + float(mx[0])) * 0.5,
|
||||||
|
(float(mn[2]) + float(mx[2])) * 0.5)
|
||||||
|
return Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------- Transform-Helfer -----
|
||||||
|
|
||||||
|
## Spiegelt die XZ-Position eines Objekts/Bau-Teils; Y (Höhe) bleibt gleich.
|
||||||
|
static func mirror_point(mode: int, p: Vector3, center: Vector2) -> Vector3:
|
||||||
|
match mode:
|
||||||
|
Mode.POINT:
|
||||||
|
return Vector3(2.0 * center.x - p.x, p.y, 2.0 * center.y - p.z)
|
||||||
|
Mode.AXIS_X:
|
||||||
|
return Vector3(2.0 * center.x - p.x, p.y, p.z)
|
||||||
|
Mode.AXIS_Z:
|
||||||
|
return Vector3(p.x, p.y, 2.0 * center.y - p.z)
|
||||||
|
_:
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
## Spiegelt die Y-Drehung in Grad. Normalisiert auf [0, 360): zwei Anwendungen
|
||||||
|
## ergeben wieder die Ausgangsdrehung (modulo 360 — gleiche Orientierung).
|
||||||
|
## Punkt: +180°, Achsen: −180°-Spiegel = Vorzeichenwechsel.
|
||||||
|
static func mirror_rot_y(mode: int, rot_y_deg: float) -> float:
|
||||||
|
match mode:
|
||||||
|
Mode.POINT:
|
||||||
|
return fposmod(rot_y_deg + 180.0, 360.0)
|
||||||
|
Mode.AXIS_X, Mode.AXIS_Z:
|
||||||
|
return fposmod(-rot_y_deg, 360.0)
|
||||||
|
_:
|
||||||
|
return rot_y_deg
|
||||||
|
|
||||||
|
|
||||||
|
## true, wenn der Punkt unter diesem Modus auf sich selbst abgebildet wird
|
||||||
|
## (Zentrum bei Punktspiegelung, Spiegelachse bei Achsenspiegelung, ± Toleranz).
|
||||||
|
## Solche Elemente werden beim gespiegelten Bauen NICHT gedoppelt.
|
||||||
|
static func maps_to_self(mode: int, p: Vector3, center: Vector2,
|
||||||
|
threshold_m: float = ON_AXIS_TOLERANCE_M) -> bool:
|
||||||
|
match mode:
|
||||||
|
Mode.POINT:
|
||||||
|
return Vector2(p.x - center.x, p.z - center.y).length() < threshold_m
|
||||||
|
Mode.AXIS_X:
|
||||||
|
return absf(p.x - center.x) < threshold_m
|
||||||
|
Mode.AXIS_Z:
|
||||||
|
return absf(p.z - center.y) < threshold_m
|
||||||
|
_:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------ Zell-Abbildung (Terrain) --
|
||||||
|
|
||||||
|
## Rasterzelle (i, j) → gespiegelte Zelle. res = terrain.resolution.
|
||||||
|
## i läuft entlang x, j entlang z (wie terrain_height_at). OFF → unverändert.
|
||||||
|
static func mirror_cell(mode: int, i: int, j: int, res: int) -> Vector2i:
|
||||||
|
if res < 2:
|
||||||
|
return Vector2i(i, j)
|
||||||
|
var last := res - 1
|
||||||
|
match mode:
|
||||||
|
Mode.POINT:
|
||||||
|
return Vector2i(last - i, last - j)
|
||||||
|
Mode.AXIS_X:
|
||||||
|
return Vector2i(last - i, j)
|
||||||
|
Mode.AXIS_Z:
|
||||||
|
return Vector2i(i, last - j)
|
||||||
|
_:
|
||||||
|
return Vector2i(i, j)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Partner-Link --
|
||||||
|
|
||||||
|
## sym-Kennung eines Elements lesen (leer = kein Partner). Typsicher: nur
|
||||||
|
## nicht-leere Strings gelten; defekte Felder werden still als „kein Partner“
|
||||||
|
## behandelt (Rückwärtskompatibilität mit alten Karten).
|
||||||
|
static func sym_of(obj: Dictionary) -> String:
|
||||||
|
var s = obj.get("sym", "")
|
||||||
|
return s if s is String else ""
|
||||||
|
|
||||||
|
|
||||||
|
## sym-Kennung setzen (mutiert das Element und gibt es zurück zum Verketten).
|
||||||
|
static func set_sym(obj: Dictionary, key: String) -> Dictionary:
|
||||||
|
obj["sym"] = key
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
## sym-Kennung entfernen (Feld komplett löschen — fehlt es, wie in alten Karten).
|
||||||
|
static func clear_sym(obj: Dictionary) -> Dictionary:
|
||||||
|
obj.erase("sym")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
## Index des Partner-Elements in einer Liste (objects ODER gebaeude):
|
||||||
|
## das Element mit derselben sym-Kennung (index selbst ausgeschlossen).
|
||||||
|
## −1 = kein Partner gefunden.
|
||||||
|
static func find_partner_index(list: Array, key: String, exclude_index: int = -1) -> int:
|
||||||
|
if key.is_empty():
|
||||||
|
return -1
|
||||||
|
for i in list.size():
|
||||||
|
if i == exclude_index or not (list[i] is Dictionary):
|
||||||
|
continue
|
||||||
|
if sym_of(list[i] as Dictionary) == key:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
## Nächste freie sym-Kennung im Dokument ("sym_1", "sym_2", …): scannt
|
||||||
|
## Objekte UND Bau-Teile, deterministic (kein Zufall, keine Uhr).
|
||||||
|
static func next_sym_key(doc: Dictionary) -> String:
|
||||||
|
var used: Dictionary = {}
|
||||||
|
for block_key in ["objects", "gebaeude"]:
|
||||||
|
var block: Array = doc.get(block_key, [])
|
||||||
|
if not (block is Array):
|
||||||
|
continue
|
||||||
|
for item in block:
|
||||||
|
if not (item is Dictionary):
|
||||||
|
continue
|
||||||
|
var s := sym_of(item as Dictionary)
|
||||||
|
if s.begins_with("sym_"):
|
||||||
|
var tail := s.substr(4)
|
||||||
|
if tail.is_valid_int():
|
||||||
|
used[int(tail)] = true
|
||||||
|
var n := 1
|
||||||
|
while used.has(n):
|
||||||
|
n += 1
|
||||||
|
return "sym_%d" % n
|
||||||
1
src/editor/symmetry_core.gd.uid
Normal file
1
src/editor/symmetry_core.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://e08hmo3p6ra3
|
||||||
|
|
@ -41,6 +41,7 @@ const MAX_HEIGHT_RANGE_M := 500.0 # V6
|
||||||
const MAX_BOUNDS_EXTENT_M := 8192.0 # V14
|
const MAX_BOUNDS_EXTENT_M := 8192.0 # V14
|
||||||
const BOUNDS_TOLERANCE_M := 1.0 # V9
|
const BOUNDS_TOLERANCE_M := 1.0 # V9
|
||||||
const MIN_SPAWN_POINTS_PER_TEAM := 4 # V11 (Entscheidung 11)
|
const MIN_SPAWN_POINTS_PER_TEAM := 4 # V11 (Entscheidung 11)
|
||||||
|
const SYM_KEY_MAX_LEN := 64 # V16 (Symmetrie-Partner-Kennung)
|
||||||
|
|
||||||
const MANIFEST_PATH := "res://library/objects.json"
|
const MANIFEST_PATH := "res://library/objects.json"
|
||||||
const JSON_INDENT := " "
|
const JSON_INDENT := " "
|
||||||
|
|
@ -279,9 +280,35 @@ static func _validate_objects(objects: Array, bounds: Dictionary, manifest: Dict
|
||||||
var rot = obj.get("rot_y_deg", 0.0)
|
var rot = obj.get("rot_y_deg", 0.0)
|
||||||
if not _is_finite_num(rot):
|
if not _is_finite_num(rot):
|
||||||
errors.append("V9: objects[%d]: rot_y_deg muss endlich sein." % i)
|
errors.append("V9: objects[%d]: rot_y_deg muss endlich sein." % i)
|
||||||
|
# V16 (Karte t_f5935023): optionales `sym` (Symmetrie-Partner-Kennung).
|
||||||
|
# Editor-Format-Erweiterung: der Match-Server ignoriert unbekannte
|
||||||
|
# Felder, die Prüfsumme läuft über die ganze Payload → kompatibel.
|
||||||
|
# Fehlt das Feld (alte Karten) → keine Prüfung, alles unverändert.
|
||||||
|
var sym = obj.get("sym", null)
|
||||||
|
if sym != null:
|
||||||
|
if not (sym is String):
|
||||||
|
errors.append("V16: objects[%d]: sym muss ein String sein (ist %s)." % [i, str(sym)])
|
||||||
|
elif (sym as String).strip_edges().is_empty():
|
||||||
|
errors.append("V16: objects[%d]: sym darf nicht leer sein." % i)
|
||||||
|
elif (sym as String).length() > SYM_KEY_MAX_LEN:
|
||||||
|
errors.append("V16: objects[%d]: sym länger als %d Zeichen." % [i, SYM_KEY_MAX_LEN])
|
||||||
# V8b: Poly-Budget (× scale² — Flächen-/Kantenwachstum der Meshes)
|
# V8b: Poly-Budget (× scale² — Flächen-/Kantenwachstum der Meshes)
|
||||||
var poly = float(lib[oid].get("poly", 0))
|
var poly = float(lib[oid].get("poly", 0))
|
||||||
total_poly += poly * float(scale) * float(scale)
|
total_poly += poly * float(scale) * float(scale)
|
||||||
|
# V16: eine sym-Kennung verbindet GENAU EIN Paar — mehr als 2 Objekte
|
||||||
|
# mit derselben Kennung sind ein Verkettungsfehler.
|
||||||
|
var sym_counts: Dictionary = {}
|
||||||
|
for obj2 in objects:
|
||||||
|
if not (obj2 is Dictionary):
|
||||||
|
continue
|
||||||
|
var s2 = (obj2 as Dictionary).get("sym", null)
|
||||||
|
if s2 is String and (s2 as String) != "":
|
||||||
|
var k := str(s2)
|
||||||
|
sym_counts[k] = int(sym_counts.get(k, 0)) + 1
|
||||||
|
for k2 in sym_counts.keys():
|
||||||
|
if int(sym_counts[k2]) > 2:
|
||||||
|
errors.append("V16: sym-Kennung '%s' verbindet %d Objekte — ein Paar hat genau 2." % [
|
||||||
|
k2, int(sym_counts[k2])])
|
||||||
if total_poly > MAX_TOTAL_POLY:
|
if total_poly > MAX_TOTAL_POLY:
|
||||||
errors.append("V8: Poly-Budget überschritten (%.0f > %d)." % [total_poly, MAX_TOTAL_POLY])
|
errors.append("V8: Poly-Budget überschritten (%.0f > %d)." % [total_poly, MAX_TOTAL_POLY])
|
||||||
return errors
|
return errors
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@ var spawn_tool # src/editor/spawn_tool.gd (Zonen, Karte t_c4162c5
|
||||||
var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5)
|
var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5)
|
||||||
var tool_mode := "objects" # "objects" | "terrain" | "spawns" | "bauen" (Modular-Builder)
|
var tool_mode := "objects" # "objects" | "terrain" | "spawns" | "bauen" (Modular-Builder)
|
||||||
|
|
||||||
|
# Symmetrie-Modus (Karte t_f5935023, M1 Fundament): Modus + Toolbar-Schalter
|
||||||
|
# + Statuszeilen-Anzeige. Standard = Punktspiegelung (Sly 20.10.). M1 legt nur
|
||||||
|
# Modus + Anzeige fest; das Spiegeln selbst kommt mit M2–M4.
|
||||||
|
var sym_menu: MenuButton
|
||||||
|
var sym_label: Label
|
||||||
|
var sym_mode: int = SymmetryCore.Mode.POINT
|
||||||
|
|
||||||
# UI-Referenzen
|
# UI-Referenzen
|
||||||
var content_tree: Tree
|
var content_tree: Tree
|
||||||
var stats_objects: Label
|
var stats_objects: Label
|
||||||
|
|
@ -182,6 +189,8 @@ func _ready() -> void:
|
||||||
manifest = _load_manifest()
|
manifest = _load_manifest()
|
||||||
_build_ui()
|
_build_ui()
|
||||||
_update_tool_label()
|
_update_tool_label()
|
||||||
|
# Symmetrie-Anzeige initialisieren (Karte t_f5935023, M1): Standard = Punkt.
|
||||||
|
_update_sym_ui()
|
||||||
# Fenster-Zustand (Karte t_297221b5): Default MAXIMIERT (kein Vollbild);
|
# Fenster-Zustand (Karte t_297221b5): Default MAXIMIERT (kein Vollbild);
|
||||||
# wer auf Fenstergröße umschaltet, bekommt die gemerkte Lage wieder.
|
# wer auf Fenstergröße umschaltet, bekommt die gemerkte Lage wieder.
|
||||||
# Close-Request selbst behandeln, damit die Lage VOR dem Quit gemerkt wird.
|
# Close-Request selbst behandeln, damit die Lage VOR dem Quit gemerkt wird.
|
||||||
|
|
@ -281,6 +290,20 @@ func _build_ui() -> void:
|
||||||
contours_btn.toggled.connect(_on_contours_toggled)
|
contours_btn.toggled.connect(_on_contours_toggled)
|
||||||
top_h.add_child(contours_btn)
|
top_h.add_child(contours_btn)
|
||||||
|
|
||||||
|
# Symmetrie-Schalter (Karte t_f5935023, M1): Aus / Punkt / Achse X / Achse Z.
|
||||||
|
# Standard = Punktspiegelung (Sly 20.10.). M1 setzt nur Modus + Anzeige;
|
||||||
|
# das gespiegelte Bauen selbst kommt mit M2–M4.
|
||||||
|
sym_menu = MenuButton.new()
|
||||||
|
sym_menu.text = "Symmetrie: Punkt"
|
||||||
|
sym_menu.flat = false
|
||||||
|
sym_menu.custom_minimum_size = UIScale.v2(160, 40)
|
||||||
|
sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt."
|
||||||
|
var sym_popup := sym_menu.get_popup()
|
||||||
|
for i in SymmetryCore.MODE_LABELS.size():
|
||||||
|
sym_popup.add_check_item(SymmetryCore.MODE_LABELS[i], i)
|
||||||
|
sym_popup.index_pressed.connect(_on_sym_mode_selected)
|
||||||
|
top_h.add_child(sym_menu)
|
||||||
|
|
||||||
# Map-Fertig-Check (Karte t_c4162c5d): Klartext-Liste VOR dem Export.
|
# Map-Fertig-Check (Karte t_c4162c5d): Klartext-Liste VOR dem Export.
|
||||||
check_btn = Button.new()
|
check_btn = Button.new()
|
||||||
check_btn.text = "✅ Karte prüfen"
|
check_btn.text = "✅ Karte prüfen"
|
||||||
|
|
@ -373,6 +396,14 @@ func _build_ui() -> void:
|
||||||
tool_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
tool_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
status_h.add_child(tool_label)
|
status_h.add_child(tool_label)
|
||||||
|
|
||||||
|
# Symmetrie-Anzeige (Karte t_f5935023, M1): Sly sieht in der Statuszeile
|
||||||
|
# immer, ob/wie gespiegelt wird (Standard: Punkt).
|
||||||
|
sym_label = Label.new()
|
||||||
|
sym_label.modulate = Color(0.55, 0.8, 1.0)
|
||||||
|
sym_label.clip_text = true
|
||||||
|
sym_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
|
status_h.add_child(sym_label)
|
||||||
|
|
||||||
stats_objects = Label.new()
|
stats_objects = Label.new()
|
||||||
stats_objects.modulate = Color(1, 1, 1, 0.75)
|
stats_objects.modulate = Color(1, 1, 1, 0.75)
|
||||||
stats_objects.clip_text = true
|
stats_objects.clip_text = true
|
||||||
|
|
@ -1847,6 +1878,30 @@ func _update_tool_label() -> void:
|
||||||
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
|
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
|
||||||
|
|
||||||
|
|
||||||
|
## Symmetrie-Schalter (Karte t_f5935023, M1): Modus aus dem Toolbar-Menü übernehmen.
|
||||||
|
func _on_sym_mode_selected(index: int) -> void:
|
||||||
|
sym_mode = clampi(index, 0, SymmetryCore.MODE_LABELS.size() - 1)
|
||||||
|
_update_sym_ui()
|
||||||
|
if sym_mode == SymmetryCore.Mode.OFF:
|
||||||
|
_set_status("Symmetrie aus — die Karte wird nicht mehr automatisch gespiegelt.")
|
||||||
|
else:
|
||||||
|
_set_status("Symmetrie: %s — Änderungen an einer Seite werden auf die andere übertragen." %
|
||||||
|
SymmetryCore.mode_label(sym_mode))
|
||||||
|
|
||||||
|
|
||||||
|
## Symmetrie-Status: Toolbar-Text + Haken + Statuszeilen-Label (M1 Fundament).
|
||||||
|
## Das tatsächliche Spiegeln von Änderungen kommt mit M2–M4.
|
||||||
|
func _update_sym_ui() -> void:
|
||||||
|
if sym_menu == null or sym_label == null:
|
||||||
|
return
|
||||||
|
var label := "Symmetrie: %s" % SymmetryCore.mode_label(sym_mode)
|
||||||
|
sym_menu.text = label
|
||||||
|
sym_label.text = label
|
||||||
|
var popup := sym_menu.get_popup()
|
||||||
|
for i in SymmetryCore.MODE_LABELS.size():
|
||||||
|
popup.set_item_checked(i, i == sym_mode)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------- History ----
|
# --------------------------------------------------------------- History ----
|
||||||
|
|
||||||
func _do_undo() -> void:
|
func _do_undo() -> void:
|
||||||
|
|
|
||||||
1
tests/probe_cache_keys.gd.uid
Normal file
1
tests/probe_cache_keys.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bay7kp4kwlr5m
|
||||||
1
tests/probe_camo_render.gd.uid
Normal file
1
tests/probe_camo_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ci6lucixnhbov
|
||||||
1
tests/probe_getimg.gd.uid
Normal file
1
tests/probe_getimg.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dn7nv0olm65f5
|
||||||
300
tests/test_symmetry_core.gd
Normal file
300
tests/test_symmetry_core.gd
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Editor-Tests (headless): Karte t_f5935023 — M1 Symmetrie-Fundament.
|
||||||
|
## Ausführen: godot --headless --path . --script res://tests/test_symmetry_core.gd
|
||||||
|
## Exit 0 = alles grün. Läuft OHNE UI (nur SymmetryCore/SvmapIO/SvmapDocument) —
|
||||||
|
## Toolbar/Statuszeile verifiziert der GUI-Smoke (--sym-demo kommt mit M2/M3).
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
var checks := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
# user:// persistiert über Godot-Läufe — vor UND nach dem Test aufräumen
|
||||||
|
# (Lektion aus t_5badf371: Stale-Reste maskierten sonst Verifikationen).
|
||||||
|
_cleanup()
|
||||||
|
_run_tests()
|
||||||
|
_cleanup()
|
||||||
|
if failures == 0:
|
||||||
|
print("ALL OK: %d checks" % checks)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("FAILED: %d/%d checks" % [failures, checks])
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _cleanup() -> void:
|
||||||
|
for path in ["user://test_sym.svmap", "user://test_sym_legacy.svmap"]:
|
||||||
|
if FileAccess.file_exists(path):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||||
|
|
||||||
|
|
||||||
|
func _check(cond: bool, label: String) -> void:
|
||||||
|
checks += 1
|
||||||
|
if cond:
|
||||||
|
print(" ok: %s" % label)
|
||||||
|
else:
|
||||||
|
failures += 1
|
||||||
|
print(" FAIL: %s" % label)
|
||||||
|
|
||||||
|
|
||||||
|
func _errors_contain(errors: Array, needle: String) -> bool:
|
||||||
|
for e in errors:
|
||||||
|
if str(e).contains(needle):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _run_tests() -> void:
|
||||||
|
_test_modes_and_labels()
|
||||||
|
_test_center()
|
||||||
|
_test_mirror_point_double_identity()
|
||||||
|
_test_mirror_rot_double_identity()
|
||||||
|
_test_maps_to_self()
|
||||||
|
_test_cell_mapping()
|
||||||
|
_test_partner_link()
|
||||||
|
_test_validator_sym()
|
||||||
|
_test_roundtrip_with_sym()
|
||||||
|
_test_legacy_map_unchanged()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Tests ---
|
||||||
|
|
||||||
|
func _test_modes_and_labels() -> void:
|
||||||
|
print("== Modus-Katalog ==")
|
||||||
|
_check(SymmetryCore.Mode.OFF == 0, "OFF = 0")
|
||||||
|
_check(SymmetryCore.Mode.POINT == 1, "POINT = 1 (Standard, Sly 20.10.)")
|
||||||
|
_check(SymmetryCore.Mode.AXIS_X == 2, "AXIS_X = 2")
|
||||||
|
_check(SymmetryCore.Mode.AXIS_Z == 3, "AXIS_Z = 3")
|
||||||
|
_check(SymmetryCore.MODE_LABELS.size() == 4, "4 Modus-Labels (Aus/Punkt/Achse X/Achse Z)")
|
||||||
|
_check(SymmetryCore.mode_label(SymmetryCore.Mode.POINT) == "Punkt", "Label für POINT")
|
||||||
|
_check(SymmetryCore.mode_label(9) == "Achse Z", "Unbekannter Modus clampt auf letzten Label")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_center() -> void:
|
||||||
|
print("== Kartenmitte ==")
|
||||||
|
var doc := SvmapDocument.create_new("SymTest", 1024.0, "mittag")
|
||||||
|
var c := SymmetryCore.map_center(doc)
|
||||||
|
_check(c.is_zero_approx(), "Zentrierte Map: Mitte = (0, 0) (bounds ±size/2)")
|
||||||
|
# Fehlende bounds → Fallback (0, 0), kein Crash.
|
||||||
|
_check(SymmetryCore.map_center({}).is_zero_approx(), "Defektes doc → Mitte (0, 0), kein Crash")
|
||||||
|
# Asymmetrische bounds: Mitte = (min + max) / 2.
|
||||||
|
var doc2 := doc.duplicate(true)
|
||||||
|
doc2["bounds"] = {"min": [-100.0, -10.0, -50.0], "max": [300.0, 80.0, 50.0]}
|
||||||
|
var c2 := SymmetryCore.map_center(doc2)
|
||||||
|
_check(absf(c2.x - 100.0) < 0.001 and absf(c2.y - 0.0) < 0.001,
|
||||||
|
"Asymmetrische bounds: Mitte (100, 0) korrekt")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_mirror_point_double_identity() -> void:
|
||||||
|
print("== Zweimal spiegeln = Ausgangslage (Position, alle 3 Modi) ==")
|
||||||
|
var doc := SvmapDocument.create_new("SymTest", 1024.0, "mittag")
|
||||||
|
var center := SymmetryCore.map_center(doc)
|
||||||
|
var probe: Vector3 = Vector3(123.5, 42.0, -77.25)
|
||||||
|
for mode in [SymmetryCore.Mode.POINT, SymmetryCore.Mode.AXIS_X, SymmetryCore.Mode.AXIS_Z]:
|
||||||
|
var once := SymmetryCore.mirror_point(mode, probe, center)
|
||||||
|
var twice := SymmetryCore.mirror_point(mode, once, center)
|
||||||
|
_check(twice.distance_to(probe) < 0.0001,
|
||||||
|
"Modus %s: mirror(mirror(p)) == p (p=%s)" % [SymmetryCore.mode_label(mode), probe])
|
||||||
|
_check(absf(once.y - probe.y) < 0.0001,
|
||||||
|
"Modus %s: y bleibt unverändert" % SymmetryCore.mode_label(mode))
|
||||||
|
# Mitte bleibt Mitte (Punktspiegelung).
|
||||||
|
var mid := Vector3(0.0, 5.0, 0.0)
|
||||||
|
var mid_m := SymmetryCore.mirror_point(SymmetryCore.Mode.POINT, mid, center)
|
||||||
|
_check(mid_m.distance_to(mid) < 0.0001, "Mitte bleibt unter Punktspiegelung fix")
|
||||||
|
# Achse X: z unverändert; Achse Z: x unverändert.
|
||||||
|
var px := SymmetryCore.mirror_point(SymmetryCore.Mode.AXIS_X, probe, center)
|
||||||
|
_check(absf(px.z - probe.z) < 0.0001, "Achse X: z unverändert")
|
||||||
|
var pz := SymmetryCore.mirror_point(SymmetryCore.Mode.AXIS_Z, probe, center)
|
||||||
|
_check(absf(pz.x - probe.x) < 0.0001, "Achse Z: x unverändert")
|
||||||
|
# OFF = Identität.
|
||||||
|
_check(SymmetryCore.mirror_point(SymmetryCore.Mode.OFF, probe, center).distance_to(probe) < 0.0001,
|
||||||
|
"OFF: Position unverändert")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_mirror_rot_double_identity() -> void:
|
||||||
|
print("== Zweimal spiegeln = Ausgangslage (Drehung, modulo 360) ==")
|
||||||
|
var rots := [0.0, 45.0, 90.0, 179.0, -90.0, 359.0, -359.0]
|
||||||
|
for mode in [SymmetryCore.Mode.POINT, SymmetryCore.Mode.AXIS_X, SymmetryCore.Mode.AXIS_Z]:
|
||||||
|
for r in rots:
|
||||||
|
var r_in: float = r
|
||||||
|
var once := SymmetryCore.mirror_rot_y(mode, r_in)
|
||||||
|
var twice := SymmetryCore.mirror_rot_y(mode, once)
|
||||||
|
var diff: float = absf(fposmod(twice - r_in + 180.0, 360.0) - 180.0)
|
||||||
|
_check(diff < 0.0001, "Modus %s: rot %.1f → zweimal → gleich (mod 360)" % [
|
||||||
|
SymmetryCore.mode_label(mode), r_in])
|
||||||
|
# Einzelfälle laut Spec: Punkt 0° → 180°, Achse 90° → −90° (≡ 270°).
|
||||||
|
_check(absf(SymmetryCore.mirror_rot_y(SymmetryCore.Mode.POINT, 0.0) - 180.0) < 0.0001,
|
||||||
|
"Spec: Punkt 0° → 180°")
|
||||||
|
_check(absf(fposmod(SymmetryCore.mirror_rot_y(SymmetryCore.Mode.AXIS_X, 90.0) + 90.0, 360.0)) < 0.0001,
|
||||||
|
"Spec: Achse X 90° → −90° (≡ 270°)")
|
||||||
|
_check(absf(SymmetryCore.mirror_rot_y(SymmetryCore.Mode.OFF, 42.0) - 42.0) < 0.0001,
|
||||||
|
"OFF: Drehung unverändert")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_maps_to_self() -> void:
|
||||||
|
print("== Achse/Zentrum: nicht gedoppelt ==")
|
||||||
|
var center := Vector2.ZERO
|
||||||
|
_check(SymmetryCore.maps_to_self(SymmetryCore.Mode.POINT, Vector3(0.01, 5.0, 0.01), center),
|
||||||
|
"Punkt: Zentrum (±0.01 m) → maps_to_self")
|
||||||
|
_check(not SymmetryCore.maps_to_self(SymmetryCore.Mode.POINT, Vector3(10.0, 5.0, 0.0), center),
|
||||||
|
"Punkt: 10 m neben der Mitte → wird gedoppelt")
|
||||||
|
_check(SymmetryCore.maps_to_self(SymmetryCore.Mode.AXIS_X, Vector3(0.0, 5.0, 123.0), center),
|
||||||
|
"Achse X: auf der Achse (x = 0) → maps_to_self")
|
||||||
|
_check(not SymmetryCore.maps_to_self(SymmetryCore.Mode.AXIS_X, Vector3(7.0, 5.0, 123.0), center),
|
||||||
|
"Achse X: 7 m neben der Achse → wird gedoppelt")
|
||||||
|
_check(SymmetryCore.maps_to_self(SymmetryCore.Mode.AXIS_Z, Vector3(5.0, 5.0, 0.0), center),
|
||||||
|
"Achse Z: auf der Achse (z = 0) → maps_to_self")
|
||||||
|
_check(SymmetryCore.maps_to_self(SymmetryCore.Mode.OFF, Vector3(9.0, 5.0, 9.0), center),
|
||||||
|
"OFF: nichts wird gedoppelt → maps_to_self")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_cell_mapping() -> void:
|
||||||
|
print("== Zell-Abbildung (Terrain-Raster) ==")
|
||||||
|
var res := 9
|
||||||
|
# Punkt: (i,j) → (res−1−i, res−1−j); zweimal = Ausgangszelle.
|
||||||
|
var c := SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 2, 7, res)
|
||||||
|
_check(c == Vector2i(6, 1), "Punkt: (2,7) → (6,1) bei res=9")
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, c.x, c.y, res) == Vector2i(2, 7),
|
||||||
|
"Punkt: zweimal spiegeln = Ausgangszelle")
|
||||||
|
# Achse X: nur i spiegelt; Achse Z: nur j spiegelt.
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_X, 2, 7, res) == Vector2i(6, 7),
|
||||||
|
"Achse X: (2,7) → (6,7)")
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_Z, 2, 7, res) == Vector2i(2, 1),
|
||||||
|
"Achse Z: (2,7) → (2,1)")
|
||||||
|
# Rastermitte (ungerade res): mittlere Zelle bleibt fix.
|
||||||
|
var mid_i := (res - 1) / 2
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, mid_i, mid_i, res) == Vector2i(mid_i, mid_i),
|
||||||
|
"Rastermitte (ungerade res) bleibt unter Punktspiegelung fix")
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_X, mid_i, 3, res) == Vector2i(mid_i, 3),
|
||||||
|
"Achse X: Mittelspalte bleibt fix")
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_Z, 3, mid_i, res) == Vector2i(3, mid_i),
|
||||||
|
"Achse Z: Mittelzeile bleibt fix")
|
||||||
|
# Gerade res: keine fixe Zelle.
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 3, 3, 8) == Vector2i(4, 4),
|
||||||
|
"Gerade res=8: (3,3) → (4,4)")
|
||||||
|
# OFF + Degenerate (res < 2) → unverändert, kein Crash.
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.OFF, 1, 2, 9) == Vector2i(1, 2), "OFF: Zelle unverändert")
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 0, 0, 1) == Vector2i(0, 0),
|
||||||
|
"res=1: Degenerate-Fall, kein Crash")
|
||||||
|
_check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 0, 0, 0) == Vector2i(0, 0),
|
||||||
|
"res=0: Degenerate-Fall, kein Crash")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_partner_link() -> void:
|
||||||
|
print("== Partner-Link (sym-Feld) ==")
|
||||||
|
var obj := {"id": "baum", "pos": [1.0, 0.0, 2.0], "rot_y_deg": 0.0}
|
||||||
|
_check(SymmetryCore.sym_of(obj) == "", "Objekt ohne sym → leere Kennung")
|
||||||
|
_check(SymmetryCore.sym_of({"id": "x", "sym": 42}) == "", "Defektes sym (Zahl) → leer, kein Crash")
|
||||||
|
var key := SymmetryCore.next_sym_key({"objects": [], "gebaeude": []})
|
||||||
|
_check(key == "sym_1", "Erste freie Kennung = sym_1")
|
||||||
|
SymmetryCore.set_sym(obj, key)
|
||||||
|
_check(SymmetryCore.sym_of(obj) == key, "set_sym setzt die Kennung")
|
||||||
|
_check(obj.has("sym"), "sym-Feld vorhanden")
|
||||||
|
SymmetryCore.clear_sym(obj)
|
||||||
|
_check(not obj.has("sym"), "clear_sym entfernt das Feld komplett (wie alte Karten)")
|
||||||
|
var doc := {"objects": [
|
||||||
|
{"id": "a", "pos": [0.0, 0.0, 0.0], "sym": "sym_1"},
|
||||||
|
{"id": "b", "pos": [0.0, 0.0, 0.0]},
|
||||||
|
{"id": "c", "pos": [0.0, 0.0, 0.0], "sym": "sym_2"},
|
||||||
|
], "gebaeude": [
|
||||||
|
{"id": "wand", "pos": [0.0, 0.0, 0.0], "sym": "sym_3"},
|
||||||
|
]}
|
||||||
|
_check(SymmetryCore.next_sym_key(doc) == "sym_4",
|
||||||
|
"next_sym_key überspringt belegte Kennungen über BEIDE Blöcke")
|
||||||
|
_check(SymmetryCore.find_partner_index(doc["objects"], "sym_2") == 2,
|
||||||
|
"find_partner_index findet den Partner")
|
||||||
|
_check(SymmetryCore.find_partner_index(doc["objects"], "sym_2", 2) == -1,
|
||||||
|
"find_partner_index schließt den Index selbst aus")
|
||||||
|
_check(SymmetryCore.find_partner_index(doc["gebaeude"], "sym_3") == 0,
|
||||||
|
"find_partner_index funktioniert auch im gebaeude-Block")
|
||||||
|
_check(SymmetryCore.find_partner_index(doc["objects"], "") == -1,
|
||||||
|
"Leere Kennung → kein Partner")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_validator_sym() -> void:
|
||||||
|
print("== Validator V16 (sym-Feld an Objekten) ==")
|
||||||
|
var doc := SvmapDocument.create_new("SymTest", 1024.0, "mittag")
|
||||||
|
doc["objects"] = [{
|
||||||
|
"id": "air_defense_001", "pos": [100.0, 0.0, 100.0], "rot_y_deg": 0.0, "scale": 1.0,
|
||||||
|
"sym": "sym_1",
|
||||||
|
}]
|
||||||
|
var errors := SvmapIO._validate_core(doc)
|
||||||
|
_check(errors.is_empty(), "Objekt mit gültiger sym-Kennung → keine Fehler (%s)" % [str(errors)])
|
||||||
|
# sym = kein String → V16.
|
||||||
|
doc["objects"][0]["sym"] = 42
|
||||||
|
errors = SvmapIO._validate_core(doc)
|
||||||
|
_check(_errors_contain(errors, "V16") and _errors_contain(errors, "muss ein String sein"),
|
||||||
|
"sym als Zahl → V16 'muss ein String sein'")
|
||||||
|
# sym leer → V16.
|
||||||
|
doc["objects"][0]["sym"] = " "
|
||||||
|
errors = SvmapIO._validate_core(doc)
|
||||||
|
_check(_errors_contain(errors, "V16") and _errors_contain(errors, "nicht leer"),
|
||||||
|
"sym leer/Whitespace → V16 'darf nicht leer sein'")
|
||||||
|
# sym zu lang → V16.
|
||||||
|
doc["objects"][0]["sym"] = "s".repeat(SvmapIO.SYM_KEY_MAX_LEN + 1)
|
||||||
|
errors = SvmapIO._validate_core(doc)
|
||||||
|
_check(_errors_contain(errors, "V16") and _errors_contain(errors, "länger"),
|
||||||
|
"sym > %d Zeichen → V16" % SvmapIO.SYM_KEY_MAX_LEN)
|
||||||
|
# Drei Objekte mit derselben Kennung → V16 (Paar = genau 2).
|
||||||
|
doc["objects"] = [
|
||||||
|
{"id": "air_defense_001", "pos": [100.0, 0.0, 100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_9"},
|
||||||
|
{"id": "air_defense_001", "pos": [-100.0, 0.0, -100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_9"},
|
||||||
|
{"id": "air_defense_001", "pos": [0.0, 0.0, 100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_9"},
|
||||||
|
]
|
||||||
|
errors = SvmapIO._validate_core(doc)
|
||||||
|
_check(_errors_contain(errors, "V16") and _errors_contain(errors, "genau 2"),
|
||||||
|
"3 Objekte mit gleicher Kennung → V16 'genau 2'")
|
||||||
|
# Zwei Objekte (echtes Paar) → ok.
|
||||||
|
doc["objects"].remove_at(2)
|
||||||
|
errors = SvmapIO._validate_core(doc)
|
||||||
|
_check(errors.is_empty(), "2 Objekte mit gleicher Kennung (Paar) → keine Fehler")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_roundtrip_with_sym() -> void:
|
||||||
|
print("== Roundtrip: speichern → laden → sym bleibt ==")
|
||||||
|
var doc := SvmapDocument.create_new("SymRT", 1024.0, "abend")
|
||||||
|
doc["objects"] = [
|
||||||
|
{"id": "air_defense_001", "pos": [100.0, 0.0, 100.0], "rot_y_deg": 45.0, "scale": 1.0, "sym": "sym_1"},
|
||||||
|
{"id": "air_defense_001", "pos": [-100.0, 0.0, -100.0], "rot_y_deg": 225.0, "scale": 1.0, "sym": "sym_1"},
|
||||||
|
]
|
||||||
|
doc["gebaeude"] = [
|
||||||
|
{"id": "wand", "pos": [2.0, 0.0, 4.0], "rot": 90.0, "punkte": [], "sym": "sym_2"},
|
||||||
|
{"id": "wand", "pos": [-2.0, 0.0, -4.0], "rot": 270.0, "punkte": [], "sym": "sym_2"},
|
||||||
|
]
|
||||||
|
var save := SvmapIO.save_to_file("user://test_sym.svmap", doc)
|
||||||
|
_check(save["ok"], "save_to_file mit sym ok: %s" % str(save["errors"]))
|
||||||
|
var load := SvmapIO.load_from_file("user://test_sym.svmap")
|
||||||
|
_check(load["ok"], "load_from_file mit sym ok: %s" % str(load["errors"]))
|
||||||
|
var doc2: Dictionary = load["data"]
|
||||||
|
_check((doc2["objects"] as Array).size() == 2, "2 Objekte im Roundtrip")
|
||||||
|
_check(str(doc2["objects"][0]["sym"]) == "sym_1", "Objekt-sym im Roundtrip erhalten")
|
||||||
|
_check(str(doc2["objects"][1]["sym"]) == "sym_1", "Partner-sym im Roundtrip erhalten")
|
||||||
|
_check((doc2["gebaeude"] as Array).size() == 2, "2 Bau-Teile im Roundtrip")
|
||||||
|
_check(str(doc2["gebaeude"][0]["sym"]) == "sym_2", "Bau-Teil-sym im Roundtrip erhalten")
|
||||||
|
# Re-Save → byte-identisch (Kanonisierung stabil).
|
||||||
|
var save2 := SvmapIO.save_to_file("user://test_sym2.svmap", doc2)
|
||||||
|
_check(save2["ok"], "Re-Save ok")
|
||||||
|
var t1 := FileAccess.get_file_as_string("user://test_sym.svmap")
|
||||||
|
var t2 := FileAccess.get_file_as_string("user://test_sym2.svmap")
|
||||||
|
_check(t1 == t2, "Re-Save ist byte-identisch (Prüfsumme über sym stabil)")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_legacy_map_unchanged() -> void:
|
||||||
|
print("== Alte Karte (ohne sym) lädt unverändert ==")
|
||||||
|
var doc := SvmapDocument.create_new("Legacy", 512.0, "nacht")
|
||||||
|
doc["objects"] = [
|
||||||
|
{"id": "air_defense_001", "pos": [50.0, 0.0, 50.0], "rot_y_deg": 10.0, "scale": 1.0},
|
||||||
|
]
|
||||||
|
var save := SvmapIO.save_to_file("user://test_sym_legacy.svmap", doc)
|
||||||
|
_check(save["ok"], "Legacy-save ok: %s" % str(save["errors"]))
|
||||||
|
var legacy_text := FileAccess.get_file_as_string("user://test_sym_legacy.svmap")
|
||||||
|
var load := SvmapIO.load_from_file("user://test_sym_legacy.svmap")
|
||||||
|
_check(load["ok"], "Legacy-load ok")
|
||||||
|
var doc2: Dictionary = load["data"]
|
||||||
|
_check((doc2["objects"] as Array).size() == 1, "Legacy: 1 Objekt")
|
||||||
|
_check(not doc2["objects"][0].has("sym"), "Legacy: kein sym-Feld entstanden")
|
||||||
|
# Roundtrip byte-identisch → alte Karten speichern sich unverändert weiter.
|
||||||
|
var save2 := SvmapIO.save_to_file("user://test_sym_legacy2.svmap", doc2)
|
||||||
|
_check(save2["ok"], "Legacy re-save ok")
|
||||||
|
var text2 := FileAccess.get_file_as_string("user://test_sym_legacy2.svmap")
|
||||||
|
_check(legacy_text == text2, "Legacy: speichern→laden→speichern byte-identisch")
|
||||||
1
tests/test_symmetry_core.gd.uid
Normal file
1
tests/test_symmetry_core.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5xqlmayrdfxn
|
||||||
Loading…
Reference in a new issue