275 lines
No EOL
11 KiB
GDScript
275 lines
No EOL
11 KiB
GDScript
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 |