scopedvr-map-core/ui/resize_map_dialog.gd
2026-11-01 19:31:33 +01:00

275 lines
No EOL
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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).
const UIScale := preload("res://core/ui/ui_scale.gd") # Karte t_297221b5
##
## 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 _scroll: ScrollContainer
var _vbox: VBoxContainer
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 = UIScale.v2i(520, 0)
unresizable = true # Der Dialog wächst selbst per Inhalt; kein Hand-Resize
# FIX 20.10.b: Inhalt in einen ScrollContainer — wächst die Live-Vorschau
# NACH dem Öffnen (z. B. Preset 256 → Warnband), kann die Fensterhöhe
# gedeckelt werden, OHNE dass Inhalt oder OK/Abbrechen verloren gehen.
# Start-Mindesthöhe ~ kompletter Normalinhalt (kein Kollaps beim Popup);
# _fit_after_layout setzt sie dynamisch auf Inhalt vs. sichtbarer Platz.
var scroll := ScrollContainer.new()
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
scroll.custom_minimum_size = UIScale.v2(500, 430)
add_child(scroll)
scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_scroll = scroll
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", UIScale.px(12))
vbox.custom_minimum_size = UIScale.v2(500, 0)
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(vbox)
_vbox = vbox
info_label = _make_label("")
vbox.add_child(info_label)
var preset_h := HBoxContainer.new()
preset_h.add_theme_constant_override("separation", UIScale.px(8))
for size in _PRESET_SIZES:
var btn := Button.new()
btn.text = "%d m" % size
btn.custom_minimum_size = UIScale.v2(0, 40)
# U7: Klartext-Tooltips (Karte t_2336c58e).
btn.tooltip_text = "Preset: Karte quadratisch %d × %d m" % [size, size]
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 (Steppe).")
hint.modulate = Color(1, 1, 1, 0.7)
vbox.add_child(hint)
preview_label = _make_label("")
preview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
# FIX 20.10.b: gleiche autowrap-Explosion wie oben — (0, 60) überschreibt die
# Mindestbreite von _make_label, deshalb hier BREITE + Höhe explizit setzen.
preview_label.custom_minimum_size = UIScale.v2(500, 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
# U7: Klartext-Tooltip (Karte t_2336c58e).
drop_check.tooltip_text = "An = Dinge außerhalb der neuen Grenze fliegen raus (Strg+Z holt sie zurück).\nAus = sie bleiben, aber die Map ist dann NICHT speicherbar."
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
# FIX 20.10. (Sly-Befund „Fenster viel zu lang, nichts bestätigbar“): ein
# autowrap-Label OHNE Mindestbreite meldet VOR dem ersten Layout eine
# explosionsartige Mindesthöhe (es wrappet bei ~1 px Breite → jedes Wort
# eine Zeile). popup_centered() nimmt genau diesen Wert als Fensterhöhe
# (Probe: 10 322 px statt ~320). Mindestbreite ~ Dialogbreite verankert
# den Umbruch an der echten Breite → Fenster normal hoch.
label.custom_minimum_size = Vector2(UIScale.px(500), 0)
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 = UIScale.v2(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 flache Steppe.")
# 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)
_fit_after_layout.call_deferred()
## Fenster im sichtbaren Bereich halten: Höhe deckeln (überzähliger Inhalt
## scrollt im ScrollContainer) und neu zentrieren — sonst rutschen
## OK/Abbrechen unter den Bildschirmrand, wenn die Live-Vorschau das Fenster
## NACH dem Öffnen wachsen lässt (Probe: Fenster blieb oben hängen, Buttons
## bei y≈940 in einem 900-px-Sichtbereich).
func _fit_dialog() -> void:
var area := _parent_area()
if area.x <= 0.0 or area.y <= 0.0:
return
var max_h := int(area.y) - 8
if size.y > max_h:
size = Vector2i(size.x, max_h)
var half := (area - Vector2(size)) * 0.5
position = Vector2(maxi(0, int(half.x)), maxi(0, int(half.y)))
## Erst das Layout (und damit das Fenster-Wachstum) abwarten, dann den
## Scrollbereich auf „Inhalt vs. sichtbarer Platz“ justieren und verankern.
func _fit_after_layout() -> void:
await get_tree().process_frame
await get_tree().process_frame
var area := _parent_area()
if area.x <= 0.0 or area.y <= 0.0:
return
# Scrollbereich: so hoch wie der Inhalt, aber nie höher als der sichtbare
# Platz (Titelleiste + OK/Abbrechen + Ränder bleiben reserviert). Ist der
# Inhalt größer, scrollt er INNEN — die Buttons bleiben immer erreichbar.
var needed := _vbox.get_combined_minimum_size().y + UIScale.px(16)
var max_body := area.y - UIScale.px(90)
_scroll.custom_minimum_size = Vector2(UIScale.px(500), minf(needed, max_body))
_fit_dialog()
func _parent_area() -> Vector2:
var p := get_parent()
if p is Control:
return (p as Control).size
if p is Window:
return (p as Window).size
return Vector2.ZERO
## 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