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+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 |
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
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")/.."
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue