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:
parent
116acd63ed
commit
2065ef2252
8 changed files with 572 additions and 3 deletions
|
|
@ -81,6 +81,36 @@ nichts geht versehentlich verloren.
|
||||||
| **Strg+O** | Map öffnen |
|
| **Strg+O** | Map öffnen |
|
||||||
| **Strg+N** | Neue Map |
|
| **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
|
## Terrain-Werkzeug (Karte 2/3) — nur aktiv, wenn das Terrain-Werkzeug an ist
|
||||||
|
|
||||||
| Eingabe | Wirkung |
|
| Eingabe | Wirkung |
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,17 @@ func clear() -> void:
|
||||||
|
|
||||||
|
|
||||||
## Tiefe Kopie der angegebenen Blöcke (VOR oder NACH einer Änderung).
|
## 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:
|
func snapshot(keys: Array) -> Dictionary:
|
||||||
var out := {}
|
var out := {}
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if doc.has(key):
|
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
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -108,7 +114,10 @@ func _restore_block(key: String, value: Variant) -> void:
|
||||||
doc.erase(key)
|
doc.erase(key)
|
||||||
return
|
return
|
||||||
# Ganzen Block (Array/Dict) in-place neu befüllen — Identität bleibt erhalten.
|
# 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:
|
if source is Array:
|
||||||
var arr: Array = doc.get(key, [])
|
var arr: Array = doc.get(key, [])
|
||||||
arr.clear()
|
arr.clear()
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ var new_dialog: AcceptDialog
|
||||||
var open_dialog: FileDialog
|
var open_dialog: FileDialog
|
||||||
var save_dialog: FileDialog
|
var save_dialog: FileDialog
|
||||||
var error_dialog: AcceptDialog
|
var error_dialog: AcceptDialog
|
||||||
|
var resize_dialog: ConfirmationDialog # „Kartengröße ändern“ (R3, t_c863b93b)
|
||||||
var error_text: Label
|
var error_text: Label
|
||||||
|
|
||||||
var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder
|
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()
|
var popup := file_menu.get_popup()
|
||||||
popup.add_item("Neue Map…", 0)
|
popup.add_item("Neue Map…", 0)
|
||||||
popup.add_item("Öffnen…", 1)
|
popup.add_item("Öffnen…", 1)
|
||||||
|
popup.add_item("Kartengröße ändern…", 6)
|
||||||
popup.add_separator()
|
popup.add_separator()
|
||||||
popup.add_item("Speichern (Strg+S)", 2)
|
popup.add_item("Speichern (Strg+S)", 2)
|
||||||
popup.add_item("Speichern unter…", 3)
|
popup.add_item("Speichern unter…", 3)
|
||||||
|
|
@ -315,6 +317,12 @@ func _build_ui() -> void:
|
||||||
new_dialog.confirmed.connect(_on_new_map_confirmed)
|
new_dialog.confirmed.connect(_on_new_map_confirmed)
|
||||||
add_child(new_dialog)
|
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 = FileDialog.new()
|
||||||
open_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
open_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||||
open_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
open_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||||
|
|
@ -983,6 +991,72 @@ func _on_menu_action(id: int) -> void:
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
5: # Karte testen
|
5: # Karte testen
|
||||||
_on_test_run()
|
_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!)
|
## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!)
|
||||||
|
|
|
||||||
200
src/ui/resize_map_dialog.gd
Normal file
200
src/ui/resize_map_dialog.gd
Normal 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
|
||||||
1
src/ui/resize_map_dialog.gd.uid
Normal file
1
src/ui/resize_map_dialog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://6bjars8usr43
|
||||||
254
tests/test_resize_dialog.gd
Normal file
254
tests/test_resize_dialog.gd
Normal 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")
|
||||||
1
tests/test_resize_dialog.gd.uid
Normal file
1
tests/test_resize_dialog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cai4sycgt5k51
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
GODOT=~/bin/godot4
|
GODOT=~/bin/godot4
|
||||||
rc=0
|
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
|
"$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
|
||||||
code=$?
|
code=$?
|
||||||
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)
|
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue