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