Map-Editor R3 (t_c863b93b): Kartengröße-Dialog + Menü + Undo-Verdrahtung — neuer Dialog „Kartengröße ändern“ (Datei-Menü, Stil wie new_map_dialog): aktuelle Größe/Auflösung, Freifelder X/Z (256–4096 clamp) + Preset-Buttons 256/512/1024/2048, Live-Preview aus resize_preview (R2): Verlust-Liste beim Verkleinern (Objekte/Bauteile/Spawn-Punkte draußen) mit Entfernen-Schalter, V9-Warnung (Drop aus = Map nicht speicherbar), Spawn-Speicher-Warnung (< 4 Punkte/Team, V11), Warnband beim Vergrößern über ~1024 m (Dichte-Deckel 513 → m/Punkt, Quest-Perf). Ausführen: resize_apply → doc IN-PLACE ersetzt (History-Referenz bleibt gültig) als EINE Undo-Einheit (kompletter doc-Snapshot), Dirty-Flag, Terrain/Viewport-Rebuild + Kamera-Refit. ActionHistory: snapshot/_restore_block vertragen jetzt skalare Ober-Keys (format_version/weather) → Ganz-doc-Snapshots möglich. Tests: neue Suite test_resize_dialog (40 Checks: Dialog-Aufbau/Clamp/Preview-Texte, Menü-Verdrahtung, Undo-Roundtrip BYTE-IDENTISCH, Redo, Drop-aus-Pfad), komplette Suite 12/12 ALL OK (960 Checks). docs/tasten.md: Abschnitt „Kartengröße ändern“.

This commit is contained in:
Hermes (godot-vr) 2026-10-17 19:01:15 +00:00
parent 116acd63ed
commit 2065ef2252
8 changed files with 572 additions and 3 deletions

View file

@ -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 |

View file

@ -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()

View file

@ -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!)

200
src/ui/resize_map_dialog.gd Normal file
View file

@ -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

View file

@ -0,0 +1 @@
uid://6bjars8usr43

254
tests/test_resize_dialog.gd Normal file
View file

@ -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")

View file

@ -0,0 +1 @@
uid://cai4sycgt5k51

View file

@ -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)