From 2065ef225259c3b98f51a862eb4c8c2a60337aaf Mon Sep 17 00:00:00 2001 From: "Hermes (godot-vr)" Date: Sat, 17 Oct 2026 19:01:15 +0000 Subject: [PATCH] =?UTF-8?q?Map-Editor=20R3=20(t=5Fc863b93b):=20Kartengr?= =?UTF-8?q?=C3=B6=C3=9Fe-Dialog=20+=20Men=C3=BC=20+=20Undo-Verdrahtung=20?= =?UTF-8?q?=E2=80=94=20neuer=20Dialog=20=E2=80=9EKartengr=C3=B6=C3=9Fe=20?= =?UTF-8?q?=C3=A4ndern=E2=80=9C=20(Datei-Men=C3=BC,=20Stil=20wie=20new=5Fm?= =?UTF-8?q?ap=5Fdialog):=20aktuelle=20Gr=C3=B6=C3=9Fe/Aufl=C3=B6sung,=20Fr?= =?UTF-8?q?eifelder=20X/Z=20(256=E2=80=934096=20clamp)=20+=20Preset-Button?= =?UTF-8?q?s=20256/512/1024/2048,=20Live-Preview=20aus=20resize=5Fpreview?= =?UTF-8?q?=20(R2):=20Verlust-Liste=20beim=20Verkleinern=20(Objekte/Bautei?= =?UTF-8?q?le/Spawn-Punkte=20drau=C3=9Fen)=20mit=20Entfernen-Schalter,=20V?= =?UTF-8?q?9-Warnung=20(Drop=20aus=20=3D=20Map=20nicht=20speicherbar),=20S?= =?UTF-8?q?pawn-Speicher-Warnung=20(<=204=20Punkte/Team,=20V11),=20Warnban?= =?UTF-8?q?d=20beim=20Vergr=C3=B6=C3=9Fern=20=C3=BCber=20~1024=20m=20(Dich?= =?UTF-8?q?te-Deckel=20513=20=E2=86=92=20m/Punkt,=20Quest-Perf).=20Ausf?= =?UTF-8?q?=C3=BChren:=20resize=5Fapply=20=E2=86=92=20doc=20IN-PLACE=20ers?= =?UTF-8?q?etzt=20(History-Referenz=20bleibt=20g=C3=BCltig)=20als=20EINE?= =?UTF-8?q?=20Undo-Einheit=20(kompletter=20doc-Snapshot),=20Dirty-Flag,=20?= =?UTF-8?q?Terrain/Viewport-Rebuild=20+=20Kamera-Refit.=20ActionHistory:?= =?UTF-8?q?=20snapshot/=5Frestore=5Fblock=20vertragen=20jetzt=20skalare=20?= =?UTF-8?q?Ober-Keys=20(format=5Fversion/weather)=20=E2=86=92=20Ganz-doc-S?= =?UTF-8?q?napshots=20m=C3=B6glich.=20Tests:=20neue=20Suite=20test=5Fresiz?= =?UTF-8?q?e=5Fdialog=20(40=20Checks:=20Dialog-Aufbau/Clamp/Preview-Texte,?= =?UTF-8?q?=20Men=C3=BC-Verdrahtung,=20Undo-Roundtrip=20BYTE-IDENTISCH,=20?= =?UTF-8?q?Redo,=20Drop-aus-Pfad),=20komplette=20Suite=2012/12=20ALL=20OK?= =?UTF-8?q?=20(960=20Checks).=20docs/tasten.md:=20Abschnitt=20=E2=80=9EKar?= =?UTF-8?q?tengr=C3=B6=C3=9Fe=20=C3=A4ndern=E2=80=9C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tasten.md | 30 ++++ src/editor/action_history.gd | 13 +- src/ui/main.gd | 74 ++++++++++ src/ui/resize_map_dialog.gd | 200 +++++++++++++++++++++++++ src/ui/resize_map_dialog.gd.uid | 1 + tests/test_resize_dialog.gd | 254 ++++++++++++++++++++++++++++++++ tests/test_resize_dialog.gd.uid | 1 + tools/run_tests.sh | 2 +- 8 files changed, 572 insertions(+), 3 deletions(-) create mode 100644 src/ui/resize_map_dialog.gd create mode 100644 src/ui/resize_map_dialog.gd.uid create mode 100644 tests/test_resize_dialog.gd create mode 100644 tests/test_resize_dialog.gd.uid diff --git a/docs/tasten.md b/docs/tasten.md index 8587e2b..a58cdd7 100644 --- a/docs/tasten.md +++ b/docs/tasten.md @@ -81,6 +81,36 @@ nichts geht versehentlich verloren. | **Strg+O** | Map öffnen | | **Strg+N** | Neue Map | +## Kartengröße ändern (Datei-Menü) + +„Kartengröße ändern…“ (Datei-Menü) öffnet einen Dialog mit Live-Vorschau: + +- **Aktuelle Größe** steht oben, darunter die **neue Breite (X) und Tiefe (Z) + in Metern** — Freifelder plus Preset-Buttons 256 / 512 / 1024 / 2048 m (beide + Achsen gleichzeitig). Erlaubt sind **256–4096 m** (Werte außerhalb werden + beim Anwenden an die Grenzen geklemmt). +- **Anker Mitte:** Die Karte bleibt am Ursprung zentriert. Das bestehende + Terrain behält seine WELTPOSITION (bilinear neu abgetastet — kein Stretch), + die neue Randzone wird flach auf Grundhöhe + Gras. Beim Vergrößern steigt + die Terrain-Auflösung automatisch mit (bis zum Format-Deckel 513 Punkte). +- **Verkleinern — Vorschau:** Der Dialog listet VORHER, was draußen liegen + würde („Beim Verkleinern liegen draußen: 5 Objekte, 2 Bauteile, 1 + Spawn-Punkt“). Per Schalter entscheidest du, ob diese beim Anwenden + **entfernt** werden (Standard: ja — Strg+Z holt sie zurück) oder über den + Rand hinaus in der Map bleiben — dann ist die Map **nicht speicherbar** + (Regel: nichts außerhalb der Match-Grenzen), bis du die Einträge verschiebst + oder löschst. Warnung, falls ein Team dadurch unter + **4 Spawn-Punkte** fällt (Maps brauchen mindestens 4 pro Team zum Speichern). +- **Vergrößern über ~1024 m — Warnband:** Ab dieser Größe wird das Terrain + grober aufgelöst (mehr Meter pro Höhenpunkt) — der Dialog weist auf die + geringere Detail-Dichte und das Quest-Perf-Budget (Objektzahl/Draw-Calls) hin. +- **Wann erlaubt?** Jederzeit — es muss nur eine Map existieren. +- **Undo:** Der Resize ist **EINE** Undo-Einheit: Terrain, Objekte, Bauteile + und Spawn-Zonen zusammen. **Strg+Z stellt den kompletten Vorher-Zustand + her** (inkl. gelöschter Objekte), Strg+Y stellt den Resize wieder her. + Nach dem Resize sind ungespeicherte Änderungen vorhanden (Speichern mit + Strg+S signiert die Prüfsumme neu). + ## Terrain-Werkzeug (Karte 2/3) — nur aktiv, wenn das Terrain-Werkzeug an ist | Eingabe | Wirkung | diff --git a/src/editor/action_history.gd b/src/editor/action_history.gd index cd4cf50..592c9d5 100644 --- a/src/editor/action_history.gd +++ b/src/editor/action_history.gd @@ -35,11 +35,17 @@ func clear() -> void: ## Tiefe Kopie der angegebenen Blöcke (VOR oder NACH einer Änderung). +## Skalare Ober-Keys (format_version, weather, …) werden als Wert kopiert — +## so kann ein Snapshot auch den GANZEN doc abdecken (Resize, R3). func snapshot(keys: Array) -> Dictionary: var out := {} for key in keys: if doc.has(key): - out[key] = doc[key].duplicate(true) + var value: Variant = doc[key] + if value is Array or value is Dictionary: + out[key] = value.duplicate(true) + else: + out[key] = value return out @@ -108,7 +114,10 @@ func _restore_block(key: String, value: Variant) -> void: doc.erase(key) return # Ganzen Block (Array/Dict) in-place neu befüllen — Identität bleibt erhalten. - var source: Variant = value.duplicate(true) + # Skalare (format_version, weather, …) werden als Wert gesetzt. + var source: Variant = value + if source is Array or source is Dictionary: + source = value.duplicate(true) if source is Array: var arr: Array = doc.get(key, []) arr.clear() diff --git a/src/ui/main.gd b/src/ui/main.gd index 046d658..e079d3a 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -137,6 +137,7 @@ var new_dialog: AcceptDialog var open_dialog: FileDialog var save_dialog: FileDialog var error_dialog: AcceptDialog +var resize_dialog: ConfirmationDialog # „Kartengröße ändern“ (R3, t_c863b93b) var error_text: Label var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder @@ -188,6 +189,7 @@ func _build_ui() -> void: var popup := file_menu.get_popup() popup.add_item("Neue Map…", 0) popup.add_item("Öffnen…", 1) + popup.add_item("Kartengröße ändern…", 6) popup.add_separator() popup.add_item("Speichern (Strg+S)", 2) popup.add_item("Speichern unter…", 3) @@ -315,6 +317,12 @@ func _build_ui() -> void: new_dialog.confirmed.connect(_on_new_map_confirmed) add_child(new_dialog) + # R3 (t_c863b93b): „Kartengröße ändern“ — Preview-Dialog, Ausführen als + # EINE Undo-Einheit in _on_resize_confirmed. + resize_dialog = preload("res://src/ui/resize_map_dialog.gd").new() + resize_dialog.confirmed.connect(_on_resize_confirmed) + add_child(resize_dialog) + open_dialog = FileDialog.new() open_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE open_dialog.access = FileDialog.ACCESS_FILESYSTEM @@ -983,6 +991,72 @@ func _on_menu_action(id: int) -> void: get_tree().quit() 5: # Karte testen _on_test_run() + 6: # Kartengröße ändern (R3, t_c863b93b) + _open_resize_dialog() + + +# ------------------------------------------------- Kartengröße ändern (R3) -- + +## Dialog öffnen: aktueller Stand + Live-Preview (Verlust-Liste/Warnband) füllen. +func _open_resize_dialog() -> void: + if doc.is_empty(): + _set_status("Erst eine Map anlegen (Datei → Neue Map), dann kann die Größe geändert werden.", true) + return + resize_dialog.setup(doc) + resize_dialog.popup_centered() + + +## „Größe ändern“ bestätigt: resize_apply liefert ein FRISCHES Dict (R1/R2 — +## reine Funktion). Der Resize wird als EINE Undo-Einheit in die History +## gehängt: before = kompletter alter doc-Snapshot, after = kompletter neuer. +## Wichtig: doc bleibt dasselbe Dictionary-Objekt (History hält eine Referenz — +## history.begin(doc) nur bei Neu/Laden) — Blöcke werden IN-PLACE ersetzt, +## wie ActionHistory._restore_block es beim Undo macht. +func _on_resize_confirmed() -> void: + var new_x: float = resize_dialog.get_new_size_x() + var new_z: float = resize_dialog.get_new_size_z() + var drop: bool = resize_dialog.get_drop_outside() + var before := history.snapshot(doc.keys()) + var result := SvmapDocument.resize_apply(doc, new_x, new_z, drop) + if result.is_empty(): + _set_status("Kartengröße ändern fehlgeschlagen — die Map wurde nicht verändert.", true) + return + var old_terrain: Dictionary = doc.get("terrain", {}) + _replace_doc_in_place(result) + dirty = true + history.commit("Kartengröße %d × %d m" % [int(new_x), int(new_z)], + before, history.snapshot(doc.keys())) + _after_structure_change() + camera.call("frame_map", maxf(new_x, new_z)) + var old_sx := float(old_terrain.get("size_x_m", 0.0)) + var old_sz := float(old_terrain.get("size_z_m", 0.0)) + var verb := "verkleinert" if (new_x < old_sx or new_z < old_sz) else "vergrößert" + _set_status("Karte auf %d × %d m %s — Strg+Z stellt den alten Zustand komplett her." % [ + int(new_x), int(new_z), verb]) + + +## Kompletten doc-Inhalt IN-PLACE durch ein frisches Ergebnis-Dict ersetzen +## (Identität von doc und seiner Arrays/Dicts bleibt erhalten — History, +## Tests und Ansichten behalten gültige Referenzen; Muster wie +## ActionHistory._restore_block, aber über ALLE Ober-Blöcke). +func _replace_doc_in_place(result: Dictionary) -> void: + for key in doc.keys(): + if not result.has(key): + doc.erase(key) + for key in result.keys(): + var source: Variant = result[key] + if source is Array: + var arr: Array = doc.get(key, []) + arr.clear() + arr.append_array((source as Array).duplicate(true)) + doc[key] = arr + elif source is Dictionary: + var dict: Dictionary = doc.get(key, {}) + dict.clear() + dict.merge((source as Dictionary).duplicate(true)) + doc[key] = dict + else: + doc[key] = source ## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!) diff --git a/src/ui/resize_map_dialog.gd b/src/ui/resize_map_dialog.gd new file mode 100644 index 0000000..b852fd2 --- /dev/null +++ b/src/ui/resize_map_dialog.gd @@ -0,0 +1,200 @@ +extends ConfirmationDialog +## „Kartengröße ändern“-Dialog (R3, Karte t_c863b93b — PLAN.md „Map-Größe +## ändern“): aktuelle Größe anzeigen, neue X/Z in Metern eingeben (Preset- +## Buttons + Freifeld), Live-Vorschau aus SvmapDocument.resize_preview (R2). +## +## Stil wie new_map_dialog.gd: alles per Code, Deutsch, Klartext-Warnungen. +## Der Dialog mutiert NICHTS — das Anwenden macht main.gd (_on_resize_confirmed) +## als EINE Undo-Einheit über resize_apply + ActionHistory-Snapshot. + +var size_x_edit: SpinBox +var size_z_edit: SpinBox +var drop_check: CheckBox +var info_label: Label # aktuelle Größe + Auflösung +var preview_label: Label # Live-Vorschau (Verlust-Liste / Warnband) + +var _old_sx := 0.0 +var _old_sz := 0.0 +var _old_res := 0 +var _doc: Dictionary = {} + +const _PRESET_SIZES := [256, 512, 1024, 2048] + + +func _init() -> void: + title = "Kartengröße ändern" + ok_button_text = "Größe ändern" + cancel_button_text = "Abbrechen" + min_size = Vector2i(520, 0) + + var vbox := VBoxContainer.new() + vbox.set_anchors_preset(Control.PRESET_FULL_RECT) + vbox.add_theme_constant_override("separation", 12) + add_child(vbox) + + info_label = _make_label("") + vbox.add_child(info_label) + + var preset_h := HBoxContainer.new() + preset_h.add_theme_constant_override("separation", 8) + for size in _PRESET_SIZES: + var btn := Button.new() + btn.text = "%d m" % size + btn.custom_minimum_size = Vector2(0, 40) + btn.pressed.connect(_on_preset.bind(float(size))) + preset_h.add_child(btn) + vbox.add_child(preset_h) + + vbox.add_child(_make_label("Neue Breite (X) in Metern:")) + size_x_edit = SpinBox.new() + _fill_spin(size_x_edit) + vbox.add_child(size_x_edit) + + vbox.add_child(_make_label("Neue Tiefe (Z) in Metern:")) + size_z_edit = SpinBox.new() + _fill_spin(size_z_edit) + vbox.add_child(size_z_edit) + + var hint := _make_label("Erlaubt: 256–4096 m. Die Karte bleibt mittig — das bestehende Terrain\nbehaftet seine Weltposition, die neue Randzone wird flach (Gras).") + hint.modulate = Color(1, 1, 1, 0.7) + vbox.add_child(hint) + + preview_label = _make_label("") + preview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + preview_label.custom_minimum_size = Vector2(0, 60) + vbox.add_child(preview_label) + + drop_check = CheckBox.new() + drop_check.text = "Draußen liegende Objekte/Bauteile/Spawn-Punkte beim Verkleinern entfernen" + drop_check.button_pressed = true + drop_check.toggled.connect(func(_p: bool) -> void: _update_preview()) + vbox.add_child(drop_check) + + size_x_edit.value_changed.connect(func(_v: float) -> void: _update_preview()) + size_z_edit.value_changed.connect(func(_v: float) -> void: _update_preview()) + + +func _make_label(text: String) -> Label: + var label := Label.new() + label.text = text + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + return label + + +func _fill_spin(spin: SpinBox) -> void: + spin.min_value = SvmapDocument.MIN_SIZE_M + spin.max_value = SvmapDocument.MAX_SIZE_M + spin.step = 1.0 + spin.suffix = " m" + spin.value = 1024.0 + spin.custom_minimum_size = Vector2(0, 40) + + +## Vom Editor vor dem Öffnen füllen: aktueller Stand + Live-Vorschau. +func setup(doc: Dictionary) -> void: + _doc = doc + var terrain: Dictionary = doc.get("terrain", {}) + _old_sx = float(terrain.get("size_x_m", 0.0)) + _old_sz = float(terrain.get("size_z_m", 0.0)) + _old_res = int(terrain.get("resolution", 0)) + info_label.text = "Aktuelle Größe: %d × %d m — Auflösung: %d × %d Höhenpunkte" % [ + int(_old_sx), int(_old_sz), _old_res, _old_res] + size_x_edit.value = _old_sx + size_z_edit.value = _old_sz + _update_preview() + + +func _on_preset(size: float) -> void: + size_x_edit.value = size + size_z_edit.value = size + + +func get_new_size_x() -> float: + return clampf(size_x_edit.value, SvmapDocument.MIN_SIZE_M, SvmapDocument.MAX_SIZE_M) + + +func get_new_size_z() -> float: + return clampf(size_z_edit.value, SvmapDocument.MIN_SIZE_M, SvmapDocument.MAX_SIZE_M) + + +func get_drop_outside() -> bool: + return drop_check.button_pressed + + +## Live-Vorschau: Verlust-Liste (R2 resize_preview), Spawn-Speicher-Warnung +## (V11 verlangt ≥ 4 Punkte/Team), Warnband bei Vergrößern über ~1024 m +## (Dichte-Leiter deckelt bei 513 Punkten → grobes Raster, Quest-Perf). +func _update_preview() -> void: + if _doc.is_empty(): + preview_label.text = "" + preview_label.modulate = Color(1, 1, 1, 0.7) + return + var new_sx := get_new_size_x() + var new_sz := get_new_size_z() + var preview: Dictionary = SvmapDocument.resize_preview(_doc, new_sx, new_sz) + var lines: Array[String] = [] + + var shrink := new_sx < _old_sx - 0.5 or new_sz < _old_sz - 0.5 + var grow := new_sx > _old_sx + 0.5 or new_sz > _old_sz + 0.5 + var out_total := (preview["objects_out"] as Array).size() \ + + (preview["parts_out"] as Array).size() \ + + (preview["spawns_out"] as Array).size() + + if shrink and out_total > 0: + var detail := "Beim Verkleinern liegen draußen: %d Objekt(e), %d Bauteil(e), %d Spawn-Punkt(e)." % [ + (preview["objects_out"] as Array).size(), + (preview["parts_out"] as Array).size(), + (preview["spawns_out"] as Array).size()] + if drop_check.button_pressed: + detail += "\nDiese werden beim Anwenden ENTFERNT (Strg+Z holt sie zurück)." + else: + detail += "\nSie bleiben in der Map, ragen aber über den Rand hinaus — die Map ist dann NICHT speicherbar (nichts darf außerhalb der Match-Grenzen liegen). Zieh sie später wieder rein oder entferne sie." + preview_label.modulate = Color(1.0, 0.75, 0.3) # Warnorange + lines.append(detail) + elif shrink: + preview_label.modulate = Color(1, 1, 1, 0.7) + lines.append("Beim Verkleinern geht nichts verloren — alles liegt innerhalb der neuen Grenzen.") + elif grow: + preview_label.modulate = Color(1, 1, 1, 0.7) + lines.append("Beim Vergrößern bleibt alles erhalten — die neue Randzone wird flaches Gras.") + + # Spawn-Speicher-Warnung (R2-Befund): V11 verlangt ≥ 4 Punkte je Team beim + # SPEICHERN — nach dem Entfernen drohen Teams unter den Deckwert. + if drop_check.button_pressed and out_total > 0: + var zones = _doc.get("spawn_zones", {}) + if zones is Dictionary: + var dropped: Dictionary = {} + for entry in (preview["spawns_out"] as Array): + if entry is Dictionary: + dropped[int(entry.get("team", -1))] = int(dropped.get(int(entry.get("team", -1)), 0)) + 1 + for team_key in (zones as Dictionary).keys(): + var zone = zones[team_key] + if not (zone is Dictionary): + continue + var kept := (zone.get("points", []) as Array).size() - int(dropped.get(int(team_key), 0)) + if kept < 4: + lines.append("Achtung: Team „%s“ hätte danach nur %d Spawn-Punkte — Maps brauchen mindestens 4 pro Team, um SPEICHERN zu können. Abhilfe: Entfernen ausstellen oder später Punkte ergänzen." % [ + str(zone.get("label", "Team %s" % str(team_key))), kept]) + preview_label.modulate = Color(1.0, 0.75, 0.3) + + # Warnband Vergrößern: über ~1024 m Kante grobt die Dichte (Deckel 513 + # Punkte) — Detail-Dichte + Quest-Perf (Objektzahl/Draw-Calls) erwähnen. + if grow: + var m_per_point := SvmapDocument.resize_m_per_point(_projected_resolution(), maxf(new_sx, new_sz)) + if m_per_point > SvmapDocument.RESIZE_DENSITY_TARGET_M: + lines.append("Achtung: Ab dieser Größe wird das Terrain grober aufgelöst (%.1f m pro Höhenpunkt statt 2 m). Die Hügel-Details werden weicher — für Quest-Perf gilt: mehr Fläche = mehr Objekte/Draw-Calls beachten." % m_per_point) + preview_label.modulate = Color(1.0, 0.75, 0.3) + + preview_label.text = "\n".join(lines) + + +## Projected new resolution nach dem Resize (gleiche Leiter wie resize_terrain, +## hier nur für die Warnband-Anzeige — kein Terrain-Resample im Dialog). +func _projected_resolution() -> int: + var res := _old_res + for step in SvmapDocument.RESIZE_RESOLUTION_LADDER: + if step < res: + continue + if maxf(get_new_size_x(), get_new_size_z()) / float(step - 1) <= SvmapDocument.RESIZE_DENSITY_TARGET_M: + return step + return SvmapIO.MAX_TERRAIN_RESOLUTION \ No newline at end of file diff --git a/src/ui/resize_map_dialog.gd.uid b/src/ui/resize_map_dialog.gd.uid new file mode 100644 index 0000000..67c203e --- /dev/null +++ b/src/ui/resize_map_dialog.gd.uid @@ -0,0 +1 @@ +uid://6bjars8usr43 diff --git a/tests/test_resize_dialog.gd b/tests/test_resize_dialog.gd new file mode 100644 index 0000000..243cbb1 --- /dev/null +++ b/tests/test_resize_dialog.gd @@ -0,0 +1,254 @@ +extends SceneTree +## Editor-Test (headless): R3 Resize-Dialog — Karte t_c863b93b. +## Ausführen: godot --headless --path . --script res://tests/test_resize_dialog.gd +## Exit 0 = alles grün. +## +## Deckt ab: +## 1. Dialog-Aufbau (Stil wie new_map_dialog): Presets, Clamp 256–4096, +## Info-Zeile mit aktueller Größe/Auflösung. +## 2. Preview-Texte (R2 resize_preview): Verlust-Liste beim Verkleinern, +## „nichts verloren“-Fall, Spawn-Speicher-Warnung (< 4 Punkte/Team, V11), +## Warnband beim Vergrößern über ~1024 m (Dichte-Leiter-Deckel). +## 3. Verdrahtung in main.gd: Menü-ID 6 öffnet Dialog, Bestätigen = resize_apply +## als EINE Undo-Einheit, Undo-Roundtrip speichert die alte Map +## BYTE-IDENTISCH, Redo stellt den Resize wieder her. + +var failures := 0 +var checks := 0 +var main: Control + + +func _initialize() -> void: + _run_all.call_deferred() + + +func _run_all() -> void: + _test_dialog_basics() + _test_preview_texts() + await _test_main_wiring() + _finish() + + +func _finish() -> void: + 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) + + +# --------------------------------------------------------------- Dialog ----- + +func _make_dialog() -> ConfirmationDialog: + var dlg: ConfirmationDialog = preload("res://src/ui/resize_map_dialog.gd").new() + return dlg + + +func _test_dialog_basics() -> void: + print("Testgruppe: Dialog-Aufbau + Clamp") + var dlg := _make_dialog() + var doc := SvmapDocument.create_new("Test", 1024.0, "mittag") + dlg.setup(doc) + _check(dlg.info_label.text.contains("1024"), "Info-Zeile zeigt aktuelle Groesse (1024 m)") + _check(dlg.info_label.text.contains("129"), "Info-Zeile zeigt Aufloesung (129 Punkte)") + _check(is_equal_approx(dlg.get_new_size_x(), 1024.0), "Freifeld startet bei aktueller Groesse") + + # Preset-Button 256: beide Achsen wandern mit (Button sitzt in der Preset-HBox). + var preset_btn: Button = null + for child in dlg.get_child(0).get_children(): + if child is HBoxContainer: + for b in (child as HBoxContainer).get_children(): + if b is Button and b.text == "256 m": + preset_btn = b + _check(preset_btn != null, "Preset-Button 256 m vorhanden") + if preset_btn != null: + preset_btn.emit_signal("pressed") + _check(is_equal_approx(dlg.get_new_size_x(), 256.0) + and is_equal_approx(dlg.get_new_size_z(), 256.0), + "Preset 256 setzt beide Achsen") + + # Clamp: SpinBox deckelt auf 256–4096. + dlg.size_x_edit.value = 99999.0 + _check(is_equal_approx(dlg.get_new_size_x(), 4096.0), "Clamp oben: 4096 m") + dlg.size_x_edit.value = 1.0 + _check(is_equal_approx(dlg.get_new_size_x(), 256.0), "Clamp unten: 256 m") + dlg.free() + + +# -------------------------------------------------------------- Preview ----- + +## Map mit Objekt + Bauteil + Spawn-Punkt draußen (X = ±5000 m) bauen. +func _doc_with_outsiders(size: float) -> Dictionary: + var doc := SvmapDocument.create_new("Preview", size, "mittag") + doc["objects"] = [ + {"id": "drinnen", "pos": [10.0, 0.0, 10.0], "rot": 0.0, "scale": 1.0}, + {"id": "draussen", "pos": [5000.0, 0.0, 0.0], "rot": 0.0, "scale": 1.0}, + ] + doc["gebaeude"] = [ + {"pos": [-30.0, 0.0, -30.0], "rot": 0.0, + "punkte": [[-2.0, 0.0, -2.0], [2.0, 0.0, 2.0]]}, + {"pos": [-5000.0, 0.0, 0.0], "rot": 0.0, + "punkte": [[-2.0, 0.0, -2.0], [2.0, 0.0, 2.0]]}, + ] + var zone0: Dictionary = doc["spawn_zones"]["0"] + var pts: Array = zone0["points"] + for i in pts.size(): + pts[i]["pos"][0] = 5000.0 + float(i) # Team Blau komplett draußen + zone0["points"] = pts + return doc + + +func _test_preview_texts() -> void: + print("Testgruppe: Preview-Texte (Verlust-Liste, Spawn-Warnung, Warnband)") + var dlg := _make_dialog() + + # 1) Verkleinern mit Verlust: Liste + Entfernen-Hinweis + Spawn-Warnung. + var doc := _doc_with_outsiders(1024.0) + dlg.setup(doc) + dlg.size_x_edit.value = 256.0 + dlg.size_z_edit.value = 256.0 + dlg._update_preview() # auerhalb des Baums feuert value_changed nicht (in-tree: live) + var text: String = dlg.preview_label.text + _check(text.contains("draußen") and text.contains("1 Objekt"), "Verlust-Liste nennt 1 Objekt draußen") + _check(text.contains("1 Bauteil"), "Verlust-Liste nennt 1 Bauteil draußen") + # 4 verschobene (Team Blau) + 4 Default-Punkte (Team Rot bei ±0,8 × 512 m) = 8 draußen. + _check(text.contains("8 Spawn-Punkt"), "Verlust-Liste nennt 8 Spawn-Punkte draußen") + _check(text.contains("ENTFERNT"), "Hinweis: draußen wird entfernt (Checkbox an)") + _check(text.contains("Team") and text.contains("4 pro Team"), "Spawn-Speicher-Warnung: Team unter 4 Punkte") + _check(dlg.get_drop_outside(), "Checkbox Entfernen ist Default-an") + + # 2) Entfernen aus: draußen bleibt, Map ist dann nicht speicherbar (V9). + dlg.drop_check.button_pressed = false + dlg._update_preview() + text = dlg.preview_label.text + _check(text.contains("bleiben") and text.contains("Rand"), "Ohne Entfernen: draußen bleibt in der Map") + _check(text.contains("NICHT speicherbar"), "Ohne Entfernen: Warnung vor V9 (Speichern blockiert)") + _check(not text.contains("ENTFERNT"), "Kein Entfernen-Hinweis bei Checkbox aus") + dlg.drop_check.button_pressed = true + + # 3) Verkleinern ohne Verlust: alles innen → Klartext „nichts verloren“. + var doc_leer := SvmapDocument.create_new("Leer", 512.0, "mittag") + var z0: Dictionary = doc_leer["spawn_zones"]["0"] + var z1: Dictionary = doc_leer["spawn_zones"]["1"] + for zone in [z0, z1]: + var pts2: Array = zone["points"] + for i in pts2.size(): + pts2[i]["pos"][0] = 5.0 * float(i) # alle nah am Zentrum + pts2[i]["pos"][2] = 0.0 # Z ebenfalls rein (Default-Z wäre ±153 m) + zone["points"] = pts2 + dlg.setup(doc_leer) + dlg.size_x_edit.value = 256.0 + dlg.size_z_edit.value = 256.0 + dlg._update_preview() + text = dlg.preview_label.text + _check(text.contains("nichts verloren"), "Verkleinern ohne Verlust: Klartext-Meldung") + + # 4) Vergrößern > 1024 m: Warnband (Dichte grobt, Quest-Perf). + dlg.setup(SvmapDocument.create_new("Grow", 512.0, "mittag")) + dlg.size_x_edit.value = 2048.0 + dlg.size_z_edit.value = 2048.0 + dlg._update_preview() + text = dlg.preview_label.text + _check(text.contains("grober") and text.contains("Quest"), "Warnband bei Grow > 1024 m (Dichte + Quest-Perf)") + _check(text.contains("flaches Gras") or text.contains("Randzone"), "Grow-Hinweis: neue Randzone wird Gras") + dlg.free() + + +# -------------------------------------------------------- main.gd-Drähte ---- + +func _test_main_wiring() -> void: + print("Testgruppe: main.gd-Verdrahtung + Undo-Roundtrip") + var scene: PackedScene = load("res://scenes/main.tscn") + _check(scene != null, "scenes/main.tscn laedt") + if scene == null: + return + main = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + + _check(main.resize_dialog != null, "Resize-Dialog in der Main-UI gebaut") + + main._start_with_new_map() + var doc: Dictionary = main.doc + _check(is_equal_approx(float(doc["terrain"]["size_x_m"]), 1024.0), "Start-Map 1024 m") + + # Team Blau komplett draußen schieben (Verkleinern 1024 → 256). + var zone0: Dictionary = doc["spawn_zones"]["0"] + var pts: Array = zone0["points"] + for i in pts.size(): + pts[i]["pos"][0] = 500.0 + float(i) # drin (±512), aber draußen bei 256 + zone0["points"] = pts + + # Referenz-Sicherheit: history hält doc als Objekt (keine Kopie). + _check(is_same(main.history.doc, main.doc), "History haelt doc per Referenz") + + # Vorher-Zustand speichern → später Byte-Identität prüfen. + var path_a := "/tmp/r3_vorher.svmap" + var path_b := "/tmp/r3_nach_undo.svmap" + var save_a := SvmapIO.save_to_file(path_a, doc) + _check(bool(save_a["ok"]), "Vorher-Zustand speicherbar") + + # Dialog öffnen (Menü-Pfad): Info-Zeile + Preview gefüllt. + main._open_resize_dialog() + _check(main.resize_dialog.visible, "Menue-Eintrag oeffnet den Dialog") + _check(main.resize_dialog.info_label.text.contains("1024"), "Dialog zeigt aktuelle Groesse") + main.resize_dialog.size_x_edit.value = 256.0 + main.resize_dialog.size_z_edit.value = 256.0 + _check(main.resize_dialog.preview_label.text.contains("draußen"), + "Live-Preview listet Verlust beim Verkleinern") + + # Bestätigen: resize_apply + EINE Undo-Einheit. + main._on_resize_confirmed() + var t: Dictionary = main.doc["terrain"] + _check(is_equal_approx(float(t["size_x_m"]), 256.0), "Terrain auf 256 m verkleinert") + _check((main.doc["spawn_zones"]["0"]["points"] as Array).is_empty(), + "Draußen liegende Spawn-Punkte entfernt (Drop an)") + _check(bool(main.dirty), "Dirty-Flag gesetzt") + _check(main.history.can_undo() and main.history.undo_desc().contains("256"), + "EINE Undo-Einheit: Karte auf 256 m") + _check(is_same(main.history.doc, main.doc), "doc-Identitaet bleibt beim Resize erhalten") + + # Strg+Z: kompletter Vorher-Zustand, Map speichert BYTE-IDENTISCH. + main._do_undo() + var t2: Dictionary = main.doc["terrain"] + _check(is_equal_approx(float(t2["size_x_m"]), 1024.0), "Undo: Terrain wieder 1024 m") + _check((main.doc["spawn_zones"]["0"]["points"] as Array).size() == 4, + "Undo: entfernte Spawn-Punkte komplett zurueck") + var save_b := SvmapIO.save_to_file(path_b, main.doc) + _check(bool(save_b["ok"]), "Nach-Undo-Zustand speicherbar") + var bytes_a := FileAccess.get_file_as_bytes(path_a) + var bytes_b := FileAccess.get_file_as_bytes(path_b) + _check(bytes_a == bytes_b, "Undo-Roundtrip: alte Map BYTE-IDENTISCH") + + # Strg+Y: Resize wieder hergestellt. + main._do_redo() + _check(is_equal_approx(float(main.doc["terrain"]["size_x_m"]), 256.0), + "Redo: Resize wiederhergestellt") + _check((main.doc["spawn_zones"]["0"]["points"] as Array).is_empty(), + "Redo: Spawn-Punkte wieder entfernt") + + # Drop aus: draußen bleibt in der Map (Dialog-Checkbox aus) — dokumentiertes + # Verhalten: Map bleibt, ist ABER nicht speicherbar (V9: Spawn außerhalb der + # Match-Grenzen); der Dialog warnt davor. + main.resize_dialog.drop_check.button_pressed = false + main.resize_dialog.size_x_edit.value = 256.0 + main.resize_dialog.size_z_edit.value = 256.0 + # Erst Undo (zurück auf 1024 mit Punkten), dann Resize ohne Drop. + main._do_undo() + var before_count := (main.doc["spawn_zones"]["0"]["points"] as Array).size() + main._on_resize_confirmed() + _check((main.doc["spawn_zones"]["0"]["points"] as Array).size() == before_count, + "Drop aus: draußen liegende Spawn-Punkte bleiben in der Map") + _check(not bool(SvmapIO.save_to_file("/tmp/r3_drop_aus.svmap", main.doc)["ok"]), + "Drop aus + Punkte draußen: Speichern blockiert (V9), wie im Dialog gewarnt") \ No newline at end of file diff --git a/tests/test_resize_dialog.gd.uid b/tests/test_resize_dialog.gd.uid new file mode 100644 index 0000000..81f4be6 --- /dev/null +++ b/tests/test_resize_dialog.gd.uid @@ -0,0 +1 @@ +uid://cai4sycgt5k51 diff --git a/tools/run_tests.sh b/tools/run_tests.sh index 273d6f4..99f2467 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." GODOT=~/bin/godot4 rc=0 -for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_gui_smoke test_rmb_vorrang; do +for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_gui_smoke test_rmb_vorrang; do "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 code=$? line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)