Beim Start des Test-Modus versteckt der Editor die Panels — Godot klappt dabei die Splitter zusammen (Lage springt z. B. [-320,0] → [160]), beim Wiedereinblenden baut er die Trenner mit Defaults wieder auf → die fein gezogenen Seitenbereiche waren weg („riesen groß"). Fix in main.gd (besitzt die UI, überlebt den Playtest-Node): beim Start alle split_offsets von MainSplit/BottomSplit sichern, im Exit-Handler deferred UND im nächsten Frame zurücksetzen (das Layout muss die Panels erst neu vermessen haben). Suite test_playtest 52/52 (mit Splitter-Regression: merken → verbiegen → Exit → unverändert), test_ui_layout 79, test_gui_smoke 92 grün.
2679 lines
No EOL
112 KiB
GDScript
2679 lines
No EOL
112 KiB
GDScript
extends Control
|
||
## ScopedVR Map-Editor — Hauptfenster (M2 UX-Fundament, Karte t_40d855d0;
|
||
## U1-Modularisierung, Karte t_eaccbfed).
|
||
##
|
||
## U1 (Unity-artiges Layout-Fundament): diese Datei ist jetzt ein DÜNNER
|
||
## KOORDINATOR — Dokument/History, Werkzeug-Umschaltung, Shortcuts, Dialoge,
|
||
## Update, Playtest. Die Oberflächen-Bereiche wohnen in eigenen Modulen unter
|
||
## src/ui/panels/ und melden sich DATENGETRIEBEN in
|
||
## src/ui/panels/registry.gd an; das Layout wird ausschließlich aus der
|
||
## Registry gebaut (Operator-Vorgabe 20.10. — die Umbau-Karten U2–U7 ändern
|
||
## je nur IHR Modul + eine Registry-Zeile, nie wieder main.gd).
|
||
##
|
||
## Layout (Schaubild in /opt/data/scopedvr/KARTEN_UI_UNITY_vc27.md):
|
||
## Toolbar oben · links Tab "Map-Inhalt" · Mitte 3D-Ansicht ·
|
||
## rechts Tabs (Inspektor/Auswahl/Map/Terrain/Prüfen) ·
|
||
## unten Assets (Objekt-Bibliothek) · Statuszeile.
|
||
## Alles über HSplit/HSplit/VSplit größenverstellbar; die Splitter-Lage
|
||
## bleibt in user://scopedvr_editor.cfg ([fenster] Abschnitt) gemerkt.
|
||
##
|
||
## Farben/Namen kommen aus theme/editor_theme.tres (einzige Farbquelle).
|
||
##
|
||
## UX-Fundament (Chef-Grundsatz Sly — map-editor-README):
|
||
## 1. Undo/Redo (Strg+Z / Strg+Y) über ActionHistory — spätere Werkzeuge
|
||
## (Terrain, Spawn-Zonen) melden sich mit snapshot/commit einfach an.
|
||
## 2. Ghost-Preview: Platzier-Vorschau schwebt transparent am Cursor
|
||
## (interaction.gd), Klick setzt das Objekt.
|
||
## 3. Auswahl-Anfassen: Klick wählt, Ziehen verschiebt,
|
||
## R ODER Mausrad dreht (15°-Raster), Entf löscht.
|
||
## 4. Helfende Meldungen: Klartext + DER NÄCHSTE SCHRITT, keine Godot-Texte.
|
||
## 5. Deutsch & groß: Schrift ≥ 14 px, klickbare Flächen ≥ 40 px Höhe.
|
||
|
||
const APP_TITLE := "ScopedVR Map-Editor"
|
||
# Shortcut-Infrastruktur (Karte t_c46d31c2): InputMap-Actions + Esc-Priorität.
|
||
const ShortcutKeys := preload("res://src/editor/shortcut_keys.gd")
|
||
const ShortcutPolicy := preload("res://src/editor/shortcut_policy.gd")
|
||
# Shortcuts-Mathe Karte 2/3 (t_55257792): Pfeil-Raster + Pinsel-Presets.
|
||
const MoveMath := preload("res://src/editor/move_math.gd")
|
||
# Kurzhilfe-Overlay Karte 3/3 (t_11b5c25b): Gesamte Tastenliste als BBCode.
|
||
const HelpText := preload("res://src/ui/help_text.gd")
|
||
# Zentrale UI-Skalierung (Karte t_297221b5): EIN Faktor für Schrift/Panels/Zeilen.
|
||
const UIScale := preload("res://src/ui/ui_scale.gd")
|
||
# U1 (t_eaccbfed): die Registry ist die EINZIGE Layout-Quelle.
|
||
const PanelRegistryScript := preload("res://src/ui/panels/registry.gd")
|
||
# Editor-Theme (U1): eine Theme-Ressource als einzige Farbquelle.
|
||
const THEME_PATH := "res://theme/editor_theme.tres"
|
||
|
||
var manifest: Dictionary = {}
|
||
var doc: Dictionary = {} # das Map-Dictionary (1:1 svmap-Format)
|
||
var current_path := "" # leer = noch nie gespeichert
|
||
var dirty := false
|
||
var grid_snap := false # Raster-Snap (1 m) an/aus — Objekt-Werkzeuge links
|
||
|
||
var history := ActionHistory.new()
|
||
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert)
|
||
var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von interaction)
|
||
var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds)
|
||
var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1)
|
||
var spawn_tool # src/editor/spawn_tool.gd (Zonen, Karte t_c4162c5d)
|
||
var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5)
|
||
var tool_mode := "objects" # "objects" | "terrain" | "spawns" | "bauen" (Modular-Builder)
|
||
|
||
# Symmetrie-Modus (Karte t_f5935023, M1): Modus + Toolbar-Schalter
|
||
# + Statuszeilen-Anzeige. Standard = Punktspiegelung (Sly 20.10.). M1 legt nur
|
||
# Modus + Anzeige fest; das Spiegeln selbst kommt mit M2–M4.
|
||
var sym_mode: int = SymmetryCore.Mode.POINT
|
||
|
||
# UI-Referenzen (Koordinator-Sicht auf die Module — Panels bauen selbst)
|
||
var status_label: Label
|
||
var file_label: Label
|
||
var help_label: Label
|
||
var view_builder: Node3D
|
||
var camera: Camera3D
|
||
var inspector: Control # Objekt-Inspektor (Karte t_bb0d11c9) — im Inspektor-Tab
|
||
|
||
# U1-Module (aus der Registry gebaut; Toolbar/Status/Kern-Widget-Referenzen
|
||
# bleiben hier, damit Tests/Tools denselben Vertrag sehen wie vorher)
|
||
var toolbar_mod # src/ui/panels/toolbar.gd
|
||
var status_mod # src/ui/panels/status_bar.gd
|
||
var selection_mod # src/ui/panels/selection_panel.gd
|
||
var map_mod # src/ui/panels/map_panel.gd
|
||
var terrain_mod # src/ui/panels/terrain_panel.gd
|
||
var check_mod # src/ui/panels/check_panel.gd
|
||
var tree_mod # src/ui/panels/scene_tree_panel.gd
|
||
var assets_mod # src/ui/panels/assets_panel.gd
|
||
var content_tree: Tree # weiter in tree_mod (Bequemlichkeits-Referenz)
|
||
var stats_objects: Label # weiter in status_mod
|
||
var stats_terrain: Label # weiter in status_mod
|
||
var tool_label: Label # weiter in status_mod
|
||
var sym_label: Label # weiter in status_mod
|
||
var undo_btn: Button # weiter in toolbar_mod
|
||
var redo_btn: Button # weiter in toolbar_mod
|
||
var grid_btn: Button # weiter in toolbar_mod
|
||
var contours_btn: Button # weiter in toolbar_mod
|
||
var size_menu: MenuButton # weiter in toolbar_mod
|
||
var sym_menu: MenuButton # weiter in toolbar_mod
|
||
var prop_name: LineEdit # weiter in map_mod
|
||
var sel_label: Label # weiter in selection_mod
|
||
var gizmo_step_box: SpinBox # weiter in selection_mod
|
||
var library_search: LineEdit # weiter in assets_mod
|
||
var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit)
|
||
var _library_group_rows: Array = [] # weiter in assets_mod (Bequemlichkeit)
|
||
var spawn_status: Label # weiter in check_mod
|
||
var terrain_info: Label # weiter in map_mod
|
||
var build_edit_btn: Button # weiter in terrain_mod
|
||
var build_panel: VBoxContainer # weiter in terrain_mod
|
||
var build_kind_buttons: Dictionary = {} # weiter in terrain_mod
|
||
var terrain_mode_lower: Button # weiter in terrain_mod (Test-Vertrag)
|
||
var terrain_size_btn_small: Button # weiter in terrain_mod
|
||
var terrain_size_btn_medium: Button # weiter in terrain_mod
|
||
var terrain_size_btn_large: Button # weiter in terrain_mod
|
||
var terrain_palette: VBoxContainer # weiter in terrain_mod
|
||
var terrain_radius_slider: HSlider # weiter in terrain_mod
|
||
var terrain_radius_label: Label # weiter in terrain_mod
|
||
var terrain_strength_slider: HSlider # weiter in terrain_mod
|
||
var terrain_mat_buttons: Array = [] # weiter in terrain_mod (Gras/Sand/Fels)
|
||
var library_open_all_btn: Button # weiter in assets_mod
|
||
var library_close_all_btn: Button # weiter in assets_mod
|
||
|
||
# Toolbar-/Panel-Zustand (gehört dem Koordinator)
|
||
var _grid_on := false # Nutzer-Wahl für das Maßstabsgitter
|
||
# Höhenlinien (Karte t_89c0ec95): _contours_on ist die NUTZER-Wahl „fixiert
|
||
# an“ (L-Taste oder Button). Ohne Fix erscheinen die Linien NUR im
|
||
# Terrain-Werkzeug (F1) — Sly-Befund 17.10.: dauerhaft sichtbar störte.
|
||
# Effektiv = _contours_fix AN ODER tool_mode == "terrain".
|
||
var _contours_on := false
|
||
|
||
# --- Test-Start (Karte t_96e3c4ee / T1b) -------------------------------------
|
||
# main hält den ZUSTAND und die Haken (Vertrag mit tests/test_playtest_spawn.gd);
|
||
# das Panel-Modul test_start_panel.gd zeigt ihn nur an (U1-Muster).
|
||
var test_start_point: Variant = null # Vector3 (nur x/z zählt) oder null
|
||
var test_start_tool # src/editor/playtest/test_start_tool.gd
|
||
var test_start_panel: Control # src/ui/panels/test_start_panel.gd (von der Prüfsuite erwartet)
|
||
var test_start_btn: Button # „🎯 Test-Start" in der Leiste (Umschalter)
|
||
var _test_start_active := false
|
||
var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder
|
||
|
||
# Rahmen-Ausziehen (Rubberband) — Overlay über dem 3D-Viewport
|
||
var rubber_overlay: Control
|
||
var rubber_rect := Rect2() # leer = Overlay aus
|
||
|
||
# Playtest (Karte t_1cdc1a72) + Lauf-Modus-Panelverweise
|
||
var playtest: Node = null # src/editor/playtest/playtest_mode.gd Instanz
|
||
var _playtest_tool_mode := "" # gemerkter Werkzeug-Modus für die Rückkehr
|
||
var ui_top_bar: Control # Top-Bar (im Lauf-Modus aus)
|
||
var ui_left_panel: Control # linke Spalte (im Lauf-Modus aus)
|
||
var ui_right_panel: Control # rechte Tab-Spalte (im Lauf-Modus aus)
|
||
var ui_status_bar: Control # Statusleiste (im Lauf-Modus aus)
|
||
var ui_dock_bottom: Control # Assets-Bereich unten (im Lauf-Modus aus)
|
||
var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier)
|
||
|
||
# Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile.
|
||
var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“
|
||
var _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird
|
||
var help_overlay: PanelContainer # H-Kurzhilfe (nicht-blockierend)
|
||
var help_overlay_title: Label
|
||
var help_overlay_text: RichTextLabel
|
||
|
||
# Editor-Selbstupdate (Karte t_d95f087a): Start-Check + Banner + „Jetzt updaten“
|
||
var update_banner: PanelContainer # Leiste „🔄 Update verfügbar …“ (versteckt)
|
||
var update_label: Label
|
||
var update_btn: Button # „Jetzt updaten“ im Banner
|
||
var check_thread: Thread # Update-Check im Hintergrund (Start/Menü)
|
||
var update_thread: Thread # Update-Ausführung (pull + Import-Lauf)
|
||
var _update_check_busy := false
|
||
var _update_running := false
|
||
var _update_result: Dictionary = {} # letzter Check (Status/Banner-Nachfrage)
|
||
|
||
# Dialoge
|
||
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
|
||
|
||
# Map-Check (Karte prüfen)
|
||
var check_dialog: AcceptDialog
|
||
var check_text: Label
|
||
|
||
# Update-Check-Ergebnis (Menü „🔄 Updates prüfen“, Sly 20.10.: nur die
|
||
# Statuszeile las sich wie „tut nichts“ → kleines Ergebnis-Fenster).
|
||
var update_dialog: AcceptDialog
|
||
var update_text: Label
|
||
|
||
# Karte testen (Karte t_b84af40f)
|
||
var test_btn: Button
|
||
|
||
# U1-Dock-Zustand: Splitter-Lagen ([fenster]-Abschnitt, gemerkt)
|
||
var _dock_offsets: Dictionary = {}
|
||
var _split_lagen_playtest: Dictionary = {} # Playtest: Trenner-Lagen (Sly-Befund 20.10.)
|
||
|
||
const GizmoScript := preload("res://src/viewport/gizmo.gd")
|
||
const INSPECTOR_PANEL := preload("res://src/ui/inspector_panel.gd")
|
||
|
||
|
||
func _ready() -> void:
|
||
# InputMap-Actions der Tastatur-Shortcuts (Karte t_c46d31c2) — VOR UI/Kamera,
|
||
# damit die Kamera-Polls (Flug W/A/S/D + Leertaste/Shift) sofort kennen.
|
||
ShortcutKeys.register()
|
||
manifest = _load_manifest()
|
||
_build_ui()
|
||
_update_tool_label()
|
||
# Symmetrie-Anzeige initialisieren (Karte t_f5935023, M1): Standard = Punkt.
|
||
_update_sym_ui()
|
||
# Fenster-Zustand (Karte t_297221b5): Default MAXIMIERT (kein Vollbild);
|
||
# wer auf Fenstergröße umschaltet, bekommt die gemerkte Lage wieder.
|
||
# Close-Request selbst behandeln, damit die Lage VOR dem Quit gemerkt wird.
|
||
get_tree().set_auto_accept_quit(false)
|
||
_apply_window_state()
|
||
_load_dock_state()
|
||
_start_with_new_map()
|
||
_maybe_start_demo_mode()
|
||
# Selbst-Update (Karte t_d95f087a): STILLER Start-Check im Hintergrund —
|
||
# nur bei „Update verfügbar“ erscheint das Banner; offline/Tunnel weg
|
||
# bleibt alles ruhig (kein Banner, keine Meldung).
|
||
_startup_update_check()
|
||
|
||
|
||
func _load_manifest() -> Dictionary:
|
||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH))
|
||
if parsed is Dictionary:
|
||
return parsed
|
||
push_error("Bibliothek library/objects.json konnte nicht geladen werden!")
|
||
return {}
|
||
|
||
|
||
# ------------------------------------------------------------------ UI-Bau --
|
||
## Layout aus der Registry (U1, Operator-Vorgabe): Top-Bar → Update-Banner →
|
||
## Haupt-WSplit (links Tabs | Mitte 3D | rechts Tabs) → unten Assets →
|
||
## Statuszeile. Kein Panel wird hier fest verdrahtet — Registry ändert das
|
||
## Layout, main.gd bleibt gleich.
|
||
|
||
func _build_ui() -> void:
|
||
var theme_res: Theme = load(THEME_PATH)
|
||
theme_res.default_font_size = UIScale.font_size()
|
||
self.theme = theme_res
|
||
|
||
var vbox := VBoxContainer.new()
|
||
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
vbox.add_theme_constant_override("separation", UIScale.px(4))
|
||
add_child(vbox)
|
||
|
||
# --- Top-Bar (Modul toolbar.gd) ---
|
||
var top := PanelContainer.new()
|
||
# _make_module hängt das Modul selbst an den Container (doppelt add_child
|
||
# würde „already has a parent“ werfen).
|
||
var top_h: Control = _make_module(toolbar_script(), top)
|
||
vbox.add_child(top)
|
||
ui_top_bar = top # Lauf-Modus (t_1cdc1a72): Top-Bar ausblenden
|
||
toolbar_mod = top_h
|
||
_pull_toolbar_refs()
|
||
_update_banner_build(vbox)
|
||
|
||
# --- Hauptbereich: links Tabs | Mitte 3D | rechts Tabs ---
|
||
var split := HSplitContainer.new()
|
||
split.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
split.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
split.split_offset = UIScale.px(-320)
|
||
split.name = "MainSplit"
|
||
vbox.add_child(split)
|
||
|
||
# Lauf-Modus (t_1cdc1a72): linke/rechte Spalte als Member — werden im
|
||
# Lauf-Modus ausgeblendet, die 3D-Ansicht (Mitte) bleibt stehen.
|
||
var left_tabs := _build_group_tabs("links")
|
||
left_tabs.name = "LeftTabs"
|
||
ui_left_panel = left_tabs
|
||
split.add_child(left_tabs)
|
||
split.add_child(_build_center_viewport())
|
||
var right_tabs := _build_group_tabs("rechts")
|
||
right_tabs.name = "RightTabs"
|
||
ui_right_panel = right_tabs
|
||
split.add_child(right_tabs)
|
||
|
||
# --- Assets unten (Registry-Gruppe "unten", U1 Auftragspunkt 4) ---
|
||
# Die Objekt-Bibliothek wohnt jetzt UNTER dem Hauptbereich (Unity-
|
||
# Projekt-Fenster-Platz). Höhe größenverstellbar über den VSplit und
|
||
# gemerkt in [fenster] (dock_unten).
|
||
var bottom_wrap := VSplitContainer.new()
|
||
bottom_wrap.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
bottom_wrap.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
bottom_wrap.name = "BottomSplit"
|
||
# WICHTIG: den Haupt-Split ERST aus der VBox trennen, dann in den VSplit
|
||
# hängen — add_child auf ein Kind mit Parent ist ein stiller Baum-Fehler
|
||
# (Node hängt dann im Nichts: Slider/Skript-Signale laufen nicht).
|
||
vbox.remove_child(split)
|
||
# VSplit-Kind 1: der Haupt-Split (links|3D|rechts) — er wächst.
|
||
split.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
bottom_wrap.add_child(split)
|
||
# VSplit-Kind 2: die Assets-Tabs.
|
||
var bottom_tabs := _build_group_tabs("unten")
|
||
bottom_tabs.name = "BottomTabs"
|
||
bottom_tabs.custom_minimum_size = UIScale.v2(0, 300)
|
||
var bottom_panel := PanelContainer.new()
|
||
bottom_panel.add_child(bottom_tabs)
|
||
bottom_wrap.add_child(bottom_panel)
|
||
# Der VSplit ersetzt den Platz des bisherigen Haupt-Splits in der VBox.
|
||
vbox.add_child(bottom_wrap)
|
||
ui_dock_bottom = bottom_wrap
|
||
|
||
# --- Statusleiste (Modul status_bar.gd) ---
|
||
var status_panel := PanelContainer.new()
|
||
var status_h: Control = _make_module(status_bar_script(), status_panel)
|
||
vbox.add_child(status_panel)
|
||
ui_status_bar = status_panel # Lauf-Modus (t_1cdc1a72): Statusleiste aus
|
||
status_mod = status_h
|
||
_pull_status_refs()
|
||
status_label = status_mod.status_label
|
||
file_label = toolbar_mod.file_label
|
||
|
||
_build_dialogs_and_overlays()
|
||
|
||
|
||
func toolbar_script() -> GDScript:
|
||
return load("res://src/ui/panels/toolbar.gd")
|
||
|
||
|
||
func status_bar_script() -> GDScript:
|
||
return load("res://src/ui/panels/status_bar.gd")
|
||
|
||
|
||
## Ein Modul (EditorPanel-Ableger) instanziieren und bauen; Rückgabe = das
|
||
## Panel selbst (Container). Fehler im Modul-Skript = fette Meldung statt
|
||
## stiller null-Referenzen.
|
||
func _make_module(script: GDScript, parent: Control) -> Control:
|
||
var mod: Control = script.new()
|
||
parent.add_child(mod)
|
||
if mod.has_method("setup"):
|
||
mod.setup(self)
|
||
if mod.has_method("build"):
|
||
mod.call("build")
|
||
return mod
|
||
|
||
|
||
func _pull_toolbar_refs() -> void:
|
||
undo_btn = toolbar_mod.undo_btn
|
||
redo_btn = toolbar_mod.redo_btn
|
||
grid_btn = toolbar_mod.grid_btn
|
||
contours_btn = toolbar_mod.contours_btn
|
||
size_menu = toolbar_mod.size_menu
|
||
sym_menu = toolbar_mod.sym_menu
|
||
# Häkchen im UI-Größe-Menü auf die aktuelle Stufe (vor U1 rief die
|
||
# Top-Bar _sync_size_menu während des Baus — jetzt nach dem Referenz-Zug).
|
||
_sync_size_menu()
|
||
|
||
|
||
func _pull_status_refs() -> void:
|
||
stats_objects = status_mod.stats_objects
|
||
stats_terrain = status_mod.stats_terrain
|
||
tool_label = status_mod.tool_label
|
||
sym_label = status_mod.sym_label
|
||
status_label = status_mod.status_label
|
||
|
||
|
||
## Eine Tab-Gruppe aus der Registry bauen (links/rechts/unten). Jeder Bereich
|
||
## erscheint GENAU EINMAL (test_ui_layout prüft das). Die Panels werden per
|
||
## preload aus dem Registry-Eintrag erzeugt.
|
||
func _build_group_tabs(gruppe: String) -> TabContainer:
|
||
var tabs := TabContainer.new()
|
||
tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
tabs.add_theme_constant_override("separation", UIScale.px(8))
|
||
var entries: Array = PanelRegistryScript.entries(gruppe)
|
||
for entry in entries:
|
||
var script: GDScript = load(str(entry["skript"]))
|
||
var panel: Control = script.new()
|
||
panel.name = str(entry["id"])
|
||
panel.setup(self)
|
||
# Der Alt-Inspektor (t_bb0d11c9) baut in setup() — build() ist dort
|
||
# nicht vorhanden; neue Module bauen in build(). Beides zulassen.
|
||
if panel.has_method("build"):
|
||
panel.call("build")
|
||
tabs.add_child(panel)
|
||
tabs.set_tab_title(panel.get_index(), str(entry["titel"]))
|
||
_panel_group_built(gruppe, tabs)
|
||
return tabs
|
||
|
||
|
||
## Gruppen-Nacharbeit: Referenzen ziehen, sobald ihre Tabs stehen.
|
||
func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
|
||
match gruppe:
|
||
"rechts":
|
||
for child in tabs.get_children():
|
||
match child.name:
|
||
"inspektor":
|
||
inspector = child
|
||
"auswahl":
|
||
selection_mod = child
|
||
"map":
|
||
map_mod = child
|
||
"terrain":
|
||
terrain_mod = child
|
||
"check":
|
||
check_mod = child
|
||
sel_label = selection_mod.sel_label
|
||
gizmo_step_box = selection_mod.gizmo_step_box
|
||
prop_name = map_mod.prop_name
|
||
spawn_status = check_mod.spawn_status
|
||
terrain_info = map_mod.terrain_info
|
||
build_edit_btn = terrain_mod.build_edit_btn
|
||
build_panel = terrain_mod.build_panel
|
||
build_kind_buttons = terrain_mod.build_kind_buttons
|
||
# Test-/Werkzeug-Vertrag (wie vor der U1-Aufteilung): die Widgets
|
||
# wohnen in den Modulen, der Koordinator hält die alten Namen
|
||
# als Bequemlichkeits-Referenzen — nichts wird doppelt gebaut.
|
||
terrain_mode_lower = terrain_mod.terrain_mode_lower
|
||
terrain_size_btn_small = terrain_mod.terrain_size_btn_small
|
||
terrain_size_btn_medium = terrain_mod.terrain_size_btn_medium
|
||
terrain_size_btn_large = terrain_mod.terrain_size_btn_large
|
||
terrain_palette = terrain_mod.terrain_palette
|
||
terrain_radius_slider = terrain_mod.terrain_radius_slider
|
||
terrain_radius_label = terrain_mod.terrain_radius_label
|
||
terrain_strength_slider = terrain_mod.terrain_strength_slider
|
||
terrain_mat_buttons = terrain_mod.terrain_mat_buttons
|
||
"links":
|
||
for child in tabs.get_children():
|
||
if child.name == "szene":
|
||
tree_mod = child
|
||
content_tree = tree_mod.content_tree
|
||
"unten":
|
||
for child in tabs.get_children():
|
||
if child.name == "assets":
|
||
assets_mod = child
|
||
library_search = assets_mod.library_search
|
||
library_buttons = assets_mod.library_buttons
|
||
_library_group_rows = assets_mod._library_group_rows
|
||
library_open_all_btn = assets_mod.library_open_all_btn
|
||
library_close_all_btn = assets_mod.library_close_all_btn
|
||
|
||
|
||
func _build_dialogs_and_overlays() -> void:
|
||
# --- Dialoge ---
|
||
new_dialog = preload("res://src/ui/new_map_dialog.gd").new()
|
||
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
|
||
open_dialog.title = "Map öffnen"
|
||
open_dialog.filters = PackedStringArray(["*.svmap ; ScopedVR Map"])
|
||
open_dialog.size = UIScale.v2i(800, 560)
|
||
open_dialog.file_selected.connect(_on_open_selected)
|
||
add_child(open_dialog)
|
||
|
||
save_dialog = FileDialog.new()
|
||
save_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||
save_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||
save_dialog.title = "Map speichern unter"
|
||
save_dialog.filters = PackedStringArray(["*.svmap ; ScopedVR Map"])
|
||
save_dialog.size = UIScale.v2i(800, 560)
|
||
save_dialog.file_selected.connect(_on_save_selected)
|
||
add_child(save_dialog)
|
||
|
||
error_dialog = AcceptDialog.new()
|
||
error_dialog.title = "Fehler"
|
||
error_dialog.ok_button_text = "Alles klar"
|
||
error_text = Label.new()
|
||
error_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
error_text.custom_minimum_size = UIScale.v2(560, 0)
|
||
error_dialog.add_child(error_text)
|
||
add_child(error_dialog)
|
||
|
||
# Map-Check-Dialog (Karte prüfen, Karte t_c4162c5d).
|
||
check_dialog = AcceptDialog.new()
|
||
check_dialog.title = "Map-Check"
|
||
check_dialog.ok_button_text = "Danke!"
|
||
check_text = Label.new()
|
||
check_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
check_text.custom_minimum_size = UIScale.v2(620, 0)
|
||
check_dialog.add_child(check_text)
|
||
add_child(check_dialog)
|
||
|
||
# Update-Check-Ergebnis (Sly 20.10.: „tut nichts“ — weil die Rückmeldung
|
||
# NUR in der Statuszeile kam und dort schnell wieder weg war). Kleines
|
||
# Fenster mit dem Klartext-Ergebnis, Stil wie der Map-Check-Dialog.
|
||
update_dialog = AcceptDialog.new()
|
||
update_dialog.title = "Update-Check"
|
||
update_dialog.ok_button_text = "Alles klar"
|
||
update_text = Label.new()
|
||
update_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
update_text.custom_minimum_size = UIScale.v2(560, 0)
|
||
update_dialog.add_child(update_text)
|
||
add_child(update_dialog)
|
||
|
||
# Karte 3/3 (t_11b5c25b): Dirty-Schutz vor „Neue Map“ / „Öffnen“ — deutsche
|
||
# Klartext-Frage, „Verwerfen“ führt die wartende Aktion aus, „Abbrechen“ tut
|
||
# nichts (Map bleibt, wie sie ist).
|
||
dirty_dialog = ConfirmationDialog.new()
|
||
dirty_dialog.title = "Ungespeicherte Änderungen"
|
||
dirty_dialog.dialog_text = "Die Map hat ungespeicherte Änderungen — beim Fortfahren gehen sie verloren.\nJetzt verwerfen und fortfahren?"
|
||
dirty_dialog.ok_button_text = "Verwerfen"
|
||
dirty_dialog.cancel_button_text = "Abbrechen"
|
||
dirty_dialog.confirmed.connect(_on_dirty_confirmed)
|
||
add_child(dirty_dialog)
|
||
|
||
# Kurzhilfe-Overlay (Karte 3/3): H schaltet es an, H/Esc aus. Kein modal
|
||
# Dialog — der Editor bleibt bedienbar, das Panel liegt nur obenauf.
|
||
help_overlay = PanelContainer.new()
|
||
var overlay_style := StyleBoxFlat.new()
|
||
# U7: Farben über das Theme (Editor/colors/help_bg, help_border).
|
||
overlay_style.bg_color = get_theme_color("help_bg", "Editor")
|
||
overlay_style.border_color = get_theme_color("help_border", "Editor")
|
||
overlay_style.set_border_width_all(2)
|
||
overlay_style.set_corner_radius_all(8)
|
||
overlay_style.set_content_margin_all(UIScale.px(14))
|
||
help_overlay.add_theme_stylebox_override("panel", overlay_style)
|
||
# Zentriertes Panel statt Vollbild: der Editor bleibt rundherum bedienbar
|
||
# (nicht-blockierend), das Panel ist groß genug für die ganze Tabelle.
|
||
# Grow BOTH: das Panel wächst SYMMETRISCH um den Mittelpunkt (sonst hängt
|
||
# seine linke obere Ecke am Zentrum und es läuft rechts/unten aus dem Bild).
|
||
help_overlay.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||
help_overlay.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||
help_overlay.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||
help_overlay.custom_minimum_size = UIScale.v2(880, 640)
|
||
help_overlay.visible = false
|
||
var overlay_box := VBoxContainer.new()
|
||
overlay_box.add_theme_constant_override("separation", UIScale.px(6))
|
||
help_overlay.add_child(overlay_box)
|
||
help_overlay_title = Label.new()
|
||
help_overlay_title.text = "Kurzhilfe — alle Tasten auf einen Blick (H oder Esc schließt)"
|
||
help_overlay_title.add_theme_font_size_override("font_size", UIScale.heading_font_size())
|
||
help_overlay_title.add_theme_color_override("font_color", get_theme_color("accent", "Editor"))
|
||
overlay_box.add_child(help_overlay_title)
|
||
help_overlay_text = RichTextLabel.new()
|
||
help_overlay_text.bbcode_enabled = true
|
||
help_overlay_text.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
help_overlay_text.custom_minimum_size = UIScale.v2(0, 0)
|
||
help_overlay_text.text = HelpText.text()
|
||
overlay_box.add_child(help_overlay_text)
|
||
add_child(help_overlay)
|
||
|
||
|
||
func _build_center_viewport() -> SubViewportContainer:
|
||
var container := SubViewportContainer.new()
|
||
container.stretch = true
|
||
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
|
||
var viewport := SubViewport.new()
|
||
viewport.own_world_3d = true
|
||
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||
container.add_child(viewport)
|
||
|
||
var world := Node3D.new()
|
||
viewport.add_child(world)
|
||
world3d = world # Lauf-Modus (t_1cdc1a72): Spieler-Body lebt in diesem Baum
|
||
|
||
view_builder = Node3D.new()
|
||
view_builder.set_script(load("res://src/viewport/map_view_builder.gd"))
|
||
world.add_child(view_builder)
|
||
|
||
camera = Camera3D.new()
|
||
camera.set_script(load("res://src/viewport/orbit_camera.gd"))
|
||
camera.current = true
|
||
world.add_child(camera)
|
||
|
||
interaction = Node3D.new()
|
||
interaction.set_script(load("res://src/editor/interaction.gd"))
|
||
world.add_child(interaction)
|
||
interaction.setup(self, camera, view_builder)
|
||
# Achsen-Gizmo (Karte t_e0120dc2) als KIND von interaction — Kinder
|
||
# bekommen _unhandled_input ZUERST, es fängt Pfeil-Klicks ab bevor
|
||
# interaction sie sieht. interaction.gd bleibt dadurch unverändert.
|
||
gizmo = GizmoScript.new()
|
||
gizmo.setup(self, camera, view_builder)
|
||
interaction.add_child(gizmo)
|
||
camera.set("wheel_filter", Callable(interaction, "wheel_blocked"))
|
||
# Freelook-Flug-Verdrahtung (Karte t_c46d31c2): Status-Meldungen der
|
||
# Kamera laufen ins Statusfeld. Karte t_2470de89: die rechte Maustaste
|
||
# gehört IMMER dem Flug — kein Filter mehr, die Werkzeuge fassen RMB
|
||
# nicht mehr an.
|
||
camera.set("status_cb", Callable(self, "_set_status"))
|
||
|
||
# Terrain-Pinsel (Karte t_9aba04f1) — eigener Eingabe-Knoten im Weltraum.
|
||
terrain_tool = Node3D.new()
|
||
terrain_tool.set_script(load("res://src/editor/terrain_tool.gd"))
|
||
world.add_child(terrain_tool)
|
||
terrain_tool.setup(self, camera, view_builder, interaction)
|
||
|
||
# Spawn-Zonen-Werkzeug (Karte t_c4162c5d) — ditto eigener Eingabe-Knoten.
|
||
spawn_tool = Node3D.new()
|
||
spawn_tool.set_script(load("res://src/editor/spawn_tool.gd"))
|
||
world.add_child(spawn_tool)
|
||
spawn_tool.setup(self, camera, view_builder, interaction)
|
||
|
||
# Bauen-Werkzeug (Karte t_97ea59c5) — ditto eigener Eingabe-Knoten.
|
||
build_tool = Node3D.new()
|
||
build_tool.set_script(load("res://src/editor/build_tool.gd"))
|
||
world.add_child(build_tool)
|
||
build_tool.setup(self, camera, view_builder, interaction)
|
||
|
||
# Test-Start-Werkzeug (T1b, Wiederanbindung 20.10.): Marker in der Karte,
|
||
# Klick setzt den Startpunkt. Lebt — wie die übrigen Werkzeuge — in main,
|
||
# gelesen wird es von test_start_tool.gd über main.test_start_point.
|
||
test_start_tool = Node3D.new()
|
||
test_start_tool.set_script(load("res://src/editor/playtest/test_start_tool.gd"))
|
||
world.add_child(test_start_tool)
|
||
test_start_tool.setup(self, camera, view_builder, interaction)
|
||
|
||
# Bedien-Hinweis im Viewport (Overlay)
|
||
help_label = Label.new()
|
||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||
help_label.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
||
help_label.offset_left = UIScale.px(12)
|
||
help_label.offset_bottom = UIScale.px(-8)
|
||
help_label.offset_top = UIScale.px(-46)
|
||
# U7: Hinweis-Farbe über Theme (Editor/colors/muted) statt modulate.
|
||
help_label.add_theme_color_override("font_color", get_theme_color("muted", "Editor"))
|
||
# Karte t_297221b5: OHNE Umbruch würde die Mindestbreite des langen Texts
|
||
# bei Stufe „Groß“ die 3D-Ansicht breiter zwingen als das Fenster ist —
|
||
# die rechte Spalte rutschte aus dem Bildschirm. Mit Umbruch bleibt die
|
||
# Mindestbreite klein, der Hinweis legt sich bei Bedarf in mehr Zeilen.
|
||
help_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
# … und wenn der umgebrochene Text höher wird als der reservierte Streifen,
|
||
# wächst er nach OBEN (in die 3D-Ansicht) statt nach unten über die
|
||
# Statuszeile (grow-Richtung = das Godot-Mittel gegen Mindestgrößen-Überlauf
|
||
# bei Anker-Controls).
|
||
help_label.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||
container.add_child(help_label)
|
||
|
||
# Rahmen-Ausziehen (Karte t_f6f29d86): halbtransparentes Rechteck über
|
||
# dem Viewport — mouse_filter IGNORE, stört keine Maus-Ereignisse.
|
||
rubber_overlay = Control.new()
|
||
rubber_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
rubber_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
rubber_overlay.draw.connect(_draw_rubberband)
|
||
rubber_overlay.visible = false
|
||
container.add_child(rubber_overlay)
|
||
return container
|
||
|
||
|
||
## Rahmen-Rechteck aktualisieren (aus interaction.gd, Viewport-Koordinaten).
|
||
func _rubberband_update(start_pos: Vector2, cur_pos: Vector2) -> void:
|
||
rubber_rect = SelectionMath.rect_from(start_pos, cur_pos)
|
||
rubber_overlay.visible = true
|
||
rubber_overlay.queue_redraw()
|
||
|
||
|
||
## Rahmen-Overlay ausblenden (Release/Abbruch).
|
||
func _rubberband_clear() -> void:
|
||
rubber_rect = Rect2()
|
||
rubber_overlay.visible = false
|
||
rubber_overlay.queue_redraw()
|
||
|
||
|
||
func _draw_rubberband() -> void:
|
||
if rubber_rect.size.x <= 0.0 or rubber_rect.size.y <= 0.0:
|
||
return
|
||
# U7: Rubberband-Farben über das Theme (Editor/colors/rubber_fill/_line).
|
||
rubber_overlay.draw_rect(rubber_rect, get_theme_color("rubber_fill", "Editor"), true)
|
||
rubber_overlay.draw_rect(rubber_rect, get_theme_color("rubber_line", "Editor"), false, 2.0)
|
||
# ------------------------------------------------------------ Map-Aktionen --
|
||
|
||
func _start_with_new_map() -> void:
|
||
doc = SvmapDocument.create_new("Neue Map", 1024.0, "mittag")
|
||
current_path = ""
|
||
dirty = false
|
||
history.begin(doc)
|
||
_refresh_all()
|
||
_set_status("Willkommen! Links ein Objekt aus der Bibliothek anklicken, dann kannst du es in der Karte platzieren. Hilfe gibt's unten in dieser Leiste.")
|
||
|
||
|
||
func _on_menu_action(id: int) -> void:
|
||
match id:
|
||
0: # Neue Map — bei ungespeicherten Änderungen erst NACHFRAGEN (Karte 3/3)
|
||
_request_lossy(func() -> void: new_dialog.popup_centered())
|
||
1: # Öffnen — ditto Dirty-Schutz
|
||
_request_lossy(func() -> void: open_dialog.popup_centered())
|
||
2: # Speichern
|
||
_save()
|
||
3: # Speichern unter
|
||
save_dialog.popup_centered()
|
||
4: # Beenden (Fensterlage vorher merken, Karte t_297221b5)
|
||
_quit_editor()
|
||
5: # Karte testen
|
||
_on_test_run()
|
||
6: # Kartengröße ändern (R3, t_c863b93b)
|
||
_open_resize_dialog()
|
||
7: # Updates prüfen (Selbst-Update, t_d95f087a)
|
||
_on_update_check_menu()
|
||
|
||
|
||
# --------------------------------------------- UI-Größe + Fenster (t_297221b5) --
|
||
|
||
## Popup-Häkchen auf die aktuelle Stufe setzen.
|
||
func _sync_size_menu() -> void:
|
||
if size_menu == null or not is_instance_valid(size_menu):
|
||
return
|
||
var popup := size_menu.get_popup()
|
||
var cur := UIScale.step_index()
|
||
for i in popup.item_count:
|
||
popup.set_item_checked(i, i == cur)
|
||
|
||
|
||
## Neue Stufe gewählt: speichern, kompletten UI-Neuaufbau mit dem neuen
|
||
## Faktor, Statusmeldung. Wirkt SOFORT — kein Neustart nötig.
|
||
func _on_ui_size_selected(index: int) -> void:
|
||
if index < 0 or index >= UIScale.step_count():
|
||
return
|
||
if index == UIScale.step_index():
|
||
return
|
||
if _update_running:
|
||
_set_status("Während eines Updates kann die UI-Größe nicht umgestellt werden — gleich danach nochmal.", true)
|
||
_sync_size_menu()
|
||
return
|
||
UIScale.set_step_index(index)
|
||
_rebuild_ui_for_scale()
|
||
_set_status("UI-Größe: %s — sofort wirksam und gespeichert (bleibt auch nach Neustart)." % UIScale.step_label(index))
|
||
|
||
|
||
## Kompletter UI-Neuaufbau mit dem aktuellen UIScale-Faktor. Die Map (doc)
|
||
## bleibt als Objekt UNBERÜHRT (Undo-History bleibt gültig); nur die Controls
|
||
## werden neu gebaut und der Zustand (Werkzeug, Gitter, Höhenlinien, Auswahl)
|
||
## wiederhergestellt.
|
||
func _rebuild_ui_for_scale() -> void:
|
||
var prev_mode := tool_mode
|
||
for child in get_children():
|
||
remove_child(child)
|
||
child.queue_free()
|
||
_build_ui()
|
||
_update_tool_label()
|
||
_clear_edit_state()
|
||
_refresh_all()
|
||
_refresh_history_buttons()
|
||
# Nutzer-Wahl Gitter/Höhenlinien wiederherstellen (Neubau setzt Defaults).
|
||
grid_btn.set_pressed_no_signal(_grid_on)
|
||
_sync_grid_to_builder()
|
||
contours_btn.set_pressed_no_signal(_contours_on)
|
||
_sync_contours_to_builder()
|
||
# Werkzeug-Modus wiederherstellen (Neubau startet bei „objects“).
|
||
if prev_mode != "objects":
|
||
tool_mode = "objects"
|
||
_set_tool_mode(prev_mode)
|
||
# Update-Banner-Zustand wiederherstellen (Start-Check kannte das Ergebnis).
|
||
if str(_update_result.get("status")) == "update_available":
|
||
_show_update_banner(str(_update_result.get("local", "")), str(_update_result.get("remote", "")))
|
||
_sync_size_menu()
|
||
|
||
|
||
## Fenster beim Start: maximiert (Default, kein Vollbild-Zwang) oder die
|
||
## gemerkte Fensterlage wiederherstellen. Headless-Läufe (Tests) haben kein
|
||
## Fenster und springen raus.
|
||
func _apply_window_state() -> void:
|
||
if DisplayServer.get_name() == "headless":
|
||
return
|
||
var cfg := ConfigFile.new()
|
||
var maximiert := true
|
||
var groesse := Vector2i(1600, 900)
|
||
var position := Vector2i()
|
||
var hat_geom := false
|
||
if cfg.load(UIScale.cfg_path()) == OK:
|
||
maximiert = bool(cfg.get_value(UIScale.WIN_SECTION, "maximiert", true))
|
||
var g: Variant = cfg.get_value(UIScale.WIN_SECTION, "groesse", null)
|
||
if g is Array and (g as Array).size() == 2:
|
||
groesse = Vector2i(int((g as Array)[0]), int((g as Array)[1]))
|
||
hat_geom = true
|
||
var p: Variant = cfg.get_value(UIScale.WIN_SECTION, "position", null)
|
||
if p is Array and (p as Array).size() == 2:
|
||
position = Vector2i(int((p as Array)[0]), int((p as Array)[1]))
|
||
if maximiert:
|
||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MAXIMIZED)
|
||
else:
|
||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||
if hat_geom:
|
||
DisplayServer.window_set_size(groesse)
|
||
DisplayServer.window_set_position(position)
|
||
|
||
|
||
## Fensterlage merken (beim Schließen): Modus + Größe + Position.
|
||
func _save_window_state() -> void:
|
||
if DisplayServer.get_name() == "headless":
|
||
return
|
||
var win := get_window()
|
||
var cfg := ConfigFile.new()
|
||
cfg.load(UIScale.cfg_path())
|
||
cfg.set_value(UIScale.WIN_SECTION, "maximiert", win.mode == Window.MODE_MAXIMIZED)
|
||
cfg.set_value(UIScale.WIN_SECTION, "groesse", [win.size.x, win.size.y])
|
||
cfg.set_value(UIScale.WIN_SECTION, "position", [win.position.x, win.position.y])
|
||
cfg.save(UIScale.cfg_path())
|
||
|
||
|
||
## Ein Ausgang für beide Beenden-Wege (Menü + Fenster-X).
|
||
func _quit_editor() -> void:
|
||
_save_window_state()
|
||
_save_dock_state()
|
||
get_tree().quit()
|
||
|
||
|
||
func _notification(what: int) -> void:
|
||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||
_quit_editor()
|
||
|
||
|
||
# ------------------------------------------------- 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()
|
||
# Karte K1 (t_950fddaa): nach Resize die GANZE Karte passend einpassen
|
||
# (Kartenmitte aus bounds — frame_map ist weg).
|
||
var rb: Dictionary = doc.get("bounds", {})
|
||
var rmn: Array = rb.get("min", [])
|
||
var rmx: Array = rb.get("max", [])
|
||
if rmn.size() == 3 and rmx.size() == 3:
|
||
camera.call("fit_to_bounds",
|
||
Vector3(rmn[0], rmn[1], rmn[2]), Vector3(rmx[0], rmx[1], rmx[2]))
|
||
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
|
||
|
||
|
||
# --------------------------------------------- Selbst-Update (t_d95f087a) ----
|
||
|
||
## Banner „🔄 Update verfügbar“ (unaufdringliche Leiste unter der Top-Bar) —
|
||
## nur sichtbar, wenn ein Check einen Update findet. Offline bleibt alles still.
|
||
func _update_banner_build(parent: Container) -> void:
|
||
update_banner = PanelContainer.new()
|
||
var style := StyleBoxFlat.new()
|
||
# U7: Banner-Farben über das Theme (Editor/colors/update_bg/_border).
|
||
style.bg_color = get_theme_color("update_bg", "Editor")
|
||
style.border_color = get_theme_color("update_border", "Editor")
|
||
style.set_border_width_all(2)
|
||
style.set_content_margin_all(UIScale.px(8))
|
||
style.set_corner_radius_all(6)
|
||
update_banner.add_theme_stylebox_override("panel", style)
|
||
var h := HBoxContainer.new()
|
||
h.add_theme_constant_override("separation", UIScale.px(14))
|
||
update_banner.add_child(h)
|
||
update_label = Label.new()
|
||
update_label.add_theme_font_size_override("font_size", UIScale.font_size())
|
||
h.add_child(update_label)
|
||
var spacer := Control.new()
|
||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
h.add_child(spacer)
|
||
update_btn = Button.new()
|
||
update_btn.text = "🔄 Jetzt updaten"
|
||
update_btn.custom_minimum_size = UIScale.v2(190, 40)
|
||
update_btn.tooltip_text = "Lädt die neue Version (git pull), importiert sie und startet den Editor neu. Bei ungespeicherten Änderungen fragt der Editor vorher."
|
||
update_btn.pressed.connect(_on_update_now)
|
||
h.add_child(update_btn)
|
||
update_banner.visible = false
|
||
parent.add_child(update_banner)
|
||
|
||
|
||
## Still Start-Check (nur Banner bei „Update verfügbar“ — offline: nichts).
|
||
func _startup_update_check() -> void:
|
||
_updater_check_async(false)
|
||
|
||
|
||
## Datei-Menü „🔄 Updates prüfen“ (ID 7): gleicher Check mit Ergebnis-Meldung.
|
||
func _on_update_check_menu() -> void:
|
||
if _update_check_busy:
|
||
_set_status("Update-Check läuft schon…")
|
||
return
|
||
_set_status("Prüfe auf Updates…")
|
||
_updater_check_async(true)
|
||
|
||
|
||
func _updater_check_async(report: bool) -> void:
|
||
_update_check_busy = true
|
||
check_thread = Thread.new()
|
||
check_thread.start(_updater_check_thread.bind(report))
|
||
|
||
|
||
func _updater_check_thread(report: bool) -> void:
|
||
var result := EditorUpdater.check_update(
|
||
EditorUpdater.git_exe(TestRun.load_cfg()),
|
||
ProjectSettings.globalize_path("res://"))
|
||
_update_result = result
|
||
call_deferred("_updater_check_done", result, report)
|
||
|
||
|
||
## Klartext-Ergebnis-Fenster für den manuellen Update-Check (Sly 20.10.:
|
||
## Statuszeile allein las sich wie „tut nichts“). Nur bei MANUELLEM Check
|
||
## (report=true), der Still-Start-Check bleibt still.
|
||
func _show_update_popup(text: String) -> void:
|
||
if update_dialog == null or not is_instance_valid(update_dialog):
|
||
return # UI wurde gerade neu aufgebaut (UI-Größe) — nichts anfassen
|
||
update_text.text = text
|
||
update_dialog.popup_centered()
|
||
|
||
|
||
func _updater_check_done(result: Dictionary, report: bool) -> void:
|
||
_update_check_busy = false
|
||
_join(check_thread)
|
||
check_thread = null
|
||
match str(result.get("status")):
|
||
"update_available":
|
||
_show_update_banner(str(result["local"]), str(result["remote"]))
|
||
if report:
|
||
_set_status("🔄 Update verfügbar: %s → %s — im Banner „Jetzt updaten“ klicken." % [
|
||
EditorUpdater.short_sha(str(result["local"])),
|
||
EditorUpdater.short_sha(str(result["remote"]))])
|
||
_show_update_popup("🔄 Update verfügbar: %s → %s.\n\nIm Banner oben „Jetzt updaten“ klicken — der Editor holt sich den Stand, baut ihn und startet neu." % [
|
||
EditorUpdater.short_sha(str(result["local"])),
|
||
EditorUpdater.short_sha(str(result["remote"]))])
|
||
"up_to_date":
|
||
if report:
|
||
_set_status("Du bist auf dem neusten Stand (%s)." % EditorUpdater.short_sha(str(result["local"])))
|
||
_show_update_popup("Du bist auf dem neusten Stand (%s).\n\nAktuelle Version ist installiert — es gibt nichts zu tun." % EditorUpdater.short_sha(str(result["local"])))
|
||
"unreachable":
|
||
if report:
|
||
_set_status("Update-Check nicht erreichbar (offline oder Tunnel weg) — später nochmal probieren.", true)
|
||
_show_update_popup("Update-Check nicht erreichbar.\n\nDer Server ist gerade nicht erreichbar (offline oder Tunnel weg) — später nochmal probieren.")
|
||
"auth_failed":
|
||
# Karte t_1457e935: Zugangsdaten fehlen → klarer Hinweis statt
|
||
# GUI-Login-Dialog (Askpass ist im Updater abgeschaltet).
|
||
if report:
|
||
_set_status(EditorUpdater.AUTH_ERROR_TEXT, true)
|
||
_show_update_popup(EditorUpdater.AUTH_ERROR_TEXT)
|
||
_:
|
||
if report:
|
||
_set_status("Update-Check fehlgeschlagen: %s" % str(result.get("error", "")), true)
|
||
_show_update_popup("Update-Check fehlgeschlagen:\n%s" % str(result.get("error", "")))
|
||
|
||
|
||
func _show_update_banner(local_sha: String, remote_sha: String) -> void:
|
||
if update_banner == null or not is_instance_valid(update_banner):
|
||
return # UI wurde gerade neu aufgebaut (UI-Größe) — nichts anfassen
|
||
update_label.text = EditorUpdater.banner_text(local_sha, remote_sha)
|
||
update_banner.visible = true
|
||
|
||
|
||
## Thread sauber einsammeln (Ergebnis kam per call_deferred — der Thread ist
|
||
## längst fertig, join ist nur Ordnung).
|
||
func _join(thread: Thread) -> void:
|
||
if thread != null and thread.is_started():
|
||
thread.wait_to_finish()
|
||
|
||
|
||
## Banner-Knopf „Jetzt updaten“: Dirty-Schutz wie Strg+O/Strg+N (derselbe
|
||
## deutsche Dialog — „Abbrechen“ bricht das Update ab), dann git pull +
|
||
## Import-Lauf + Neustart. Import-Fehler = Abbruch mit Meldung, KEIN Neustart
|
||
## (Lektion 17.10.: sonst sieht Sly einen Editor ohne Map-Ansicht).
|
||
func _on_update_now() -> void:
|
||
if _update_running:
|
||
_set_status("Ein Update läuft schon…")
|
||
return
|
||
_request_lossy(_start_update)
|
||
|
||
|
||
func _start_update() -> void:
|
||
_update_running = true
|
||
update_btn.disabled = true
|
||
_set_status("Update läuft — neue Version wird geladen (git pull)…")
|
||
update_thread = Thread.new()
|
||
update_thread.start(_updater_run_thread)
|
||
|
||
|
||
func _updater_run_thread() -> void:
|
||
var project_dir := ProjectSettings.globalize_path("res://")
|
||
var cfg := TestRun.load_cfg()
|
||
var pull := EditorUpdater.pull(EditorUpdater.git_exe(cfg), project_dir)
|
||
if not bool(pull["ok"]):
|
||
call_deferred("_update_finished", false, "Update abgebrochen — git pull fehlgeschlagen:", pull["errors"])
|
||
return
|
||
# Karte t_f73a1cc5: Wenn der Pull eigene Dateien automatisch beiseite-
|
||
# gelegt hat, zeigt die Statuszeile den Klartext statt der technischen
|
||
# Import-Meldung (KEINE rohe Git-Zeile).
|
||
var info := str(pull.get("info", ""))
|
||
if info.is_empty():
|
||
info = "Update geladen — Version wird importiert (Godot-Import-Lauf)…"
|
||
call_deferred("_set_status", info)
|
||
var godot := TestRun.find_godot(cfg)
|
||
if godot.is_empty():
|
||
call_deferred("_update_finished", false, "Update abgebrochen — kein Godot-Programm für den Import-Lauf:", [
|
||
"Der Editor läuft nicht über ein Godot-Binary und es ist kein godot_path gesetzt.",
|
||
"Trage in scopedvr_editor.cfg (Godot-App-Daten) ein:",
|
||
" [test]",
|
||
" godot_path=\"/pfad/zu/godot\"",
|
||
])
|
||
return
|
||
var imp := EditorUpdater.run_import(godot, project_dir)
|
||
if not bool(imp["ok"]):
|
||
call_deferred("_update_finished", false, "Update ABGEBROCHEN — der Import ist fehlgeschlagen, es wird NICHT neu gestartet:", imp["errors"])
|
||
return
|
||
call_deferred("_update_finished", true, "", [])
|
||
|
||
|
||
func _update_finished(ok: bool, title: String, details: Array) -> void:
|
||
_update_running = false
|
||
if not is_instance_valid(update_btn) or not is_instance_valid(update_banner):
|
||
return # UI wurde zwischendurch neu aufgebaut (UI-Größe) — nichts anfassen
|
||
update_btn.disabled = false
|
||
_join(update_thread)
|
||
update_thread = null
|
||
if not ok:
|
||
update_banner.visible = true
|
||
_show_errors(title, details)
|
||
_set_status("Update fehlgeschlagen — der Editor bleibt unverändert (es ist nichts kaputt gemacht).", true)
|
||
return
|
||
var restart := EditorUpdater.resolve_restart(TestRun.load_cfg())
|
||
var project_dir := ProjectSettings.globalize_path("res://")
|
||
var pid := -1
|
||
if str(restart["mode"]) == "command":
|
||
var argv: PackedStringArray = restart["argv"]
|
||
pid = OS.create_process(str(argv[0]), argv.slice(1))
|
||
else:
|
||
var godot := TestRun.find_godot(TestRun.load_cfg())
|
||
if godot.is_empty():
|
||
_show_errors("Update installiert — Neustart nicht möglich:", [
|
||
"Der neue Stand ist geladen, aber das Godot-Programm für den Neustart ist unbekannt.",
|
||
"Editor einfach einmal selbst neu starten.",
|
||
])
|
||
_set_status("Update installiert — bitte den Editor einmal selbst neu starten.")
|
||
return
|
||
pid = OS.create_process(godot, EditorUpdater.self_restart_args(project_dir))
|
||
if pid <= 0:
|
||
_show_errors("Update installiert — Neustart fehlgeschlagen:", [
|
||
"Der neue Stand ist geladen, aber der Neustart wurde vom System abgelehnt.",
|
||
"Editor einfach einmal selbst neu starten.",
|
||
])
|
||
_set_status("Update installiert — bitte den Editor einmal selbst neu starten.")
|
||
return
|
||
_set_status("Update installiert (%s) — Editor startet neu…" % EditorUpdater.short_sha(str(_update_result.get("remote", ""))))
|
||
await get_tree().create_timer(1.0).timeout
|
||
get_tree().quit()
|
||
|
||
|
||
## „▶ Karte testen" (Karte t_1cdc1a72): Lauf-Modus IM Map-Builder selbst —
|
||
## erste Person, WASD + Maus, ohne Client-Ordner/VR/Server (Slys verbindlicher
|
||
## Wunsch: der Map-Builder ist überall installierbar und testet die Map ohne
|
||
## jegliche ScopedVR-Client-Abhängigkeit). Der ALTE Client-Start (Export nach
|
||
## Temp + eigener Client-Prozess, Karte t_b84af40f) bleibt als „alt" in
|
||
## src/editor/test_run.gd — der Rückbau kommt in einer späteren Karte.
|
||
func _on_test_run() -> void:
|
||
if doc.is_empty():
|
||
_set_status("Erst eine Map anlegen (Datei → Neue Map), dann testen.", true)
|
||
return
|
||
_start_playtest()
|
||
|
||
|
||
## Lauf-Modus anwerfen: Werkzeuge einfrieren, Panels ausblenden, Maus einfangen,
|
||
## PlaytestMode in der 3D-Welt anlegen (Spieler am ersten Spawnpunkt der Map).
|
||
func _start_playtest() -> void:
|
||
if playtest != null:
|
||
return
|
||
# Editor-Zustand einfrieren (ohne Auswahl/Dokument/History anzufassen):
|
||
# laufende Ghost-Platzierung zurückrollen, Gizmo-Zug abbrechen (beides
|
||
# rollt ohne History-Eintrag zurück), Werkzeug-Modus auf Auswahl umschalten.
|
||
interaction.call("cancel_placement")
|
||
if gizmo != null and gizmo.has_method("cancel_drag_if_active"):
|
||
gizmo.call("cancel_drag_if_active")
|
||
_playtest_tool_mode = tool_mode
|
||
tool_mode = "objects"
|
||
_playtest_set_frozen(true)
|
||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||
playtest = load("res://src/editor/playtest/playtest_mode.gd").new()
|
||
playtest.ui_panels = [ui_top_bar, ui_left_panel, ui_right_panel, ui_status_bar]
|
||
# Trenner-Lagen JETZT sichern (Sly-Befund 20.10.: beim Verstecken der
|
||
# Panels klappen die Trenner zusammen — nach dem Playtest mussten die
|
||
# Seitenbereiche neu gezogen werden). Wiederherstellung im Exit-Handler.
|
||
_split_lagen_playtest.clear()
|
||
for sname in ["MainSplit", "BottomSplit"]:
|
||
var sp := _find_split(sname)
|
||
if sp != null:
|
||
_split_lagen_playtest[sname] = sp.split_offsets.duplicate()
|
||
playtest.ui_status = status_label
|
||
playtest.help_label = help_label
|
||
playtest.exit_requested.connect(_on_playtest_exit_requested)
|
||
world3d.add_child(playtest)
|
||
camera.current = false # Editor-Orbit abgeben — die Spiel-Kamera übernimmt
|
||
# Karte t_96e3c4ee: Fallback-Reihenfolge mit Klartext-Hinweis — gesetzter
|
||
# Test-Start → erster Team-Spawnpunkt → Kartenmitte.
|
||
var resolved: Dictionary = TestStart.resolve_start(doc, test_start_point)
|
||
playtest.enter(doc, view_builder, resolved["pos"])
|
||
_set_status("▶ Testlauf läuft: WASD laufen, Shift rennen, Space springen, Maus umsehen — ESC zurück zum Editor. (%s)" % str(resolved["status"]))
|
||
|
||
|
||
## Editor-Input einfrieren/freigeben (Kamera-Flug, Werkzeuge, Gizmo).
|
||
## Nur auf die Werkzeug-Nodes — die Sicht (view_builder) bleibt 1:1 stehen.
|
||
func _playtest_set_frozen(on: bool) -> void:
|
||
for t in [camera, interaction, terrain_tool, spawn_tool, build_tool, test_start_tool]:
|
||
if t == null:
|
||
continue
|
||
t.set_process_unhandled_input(not on)
|
||
t.set_process(not on)
|
||
if gizmo != null:
|
||
gizmo.set_process_unhandled_input(not on)
|
||
gizmo.set_process(not on)
|
||
gizmo.visible = not on
|
||
|
||
|
||
## ESC im Lauf-Modus: sauber zurück in den Editor — Werkzeuge + Panels wieder
|
||
## an, Maus frei, Kamera + Auswahl unverändert, Dokument + Undo-Stapel unverändert.
|
||
func _on_playtest_exit_requested() -> void:
|
||
if playtest == null:
|
||
return
|
||
var mode: Node = playtest
|
||
playtest = null
|
||
mode.exit()
|
||
mode.queue_free()
|
||
_playtest_set_frozen(false)
|
||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||
camera.current = true
|
||
tool_mode = _playtest_tool_mode
|
||
_update_tool_label()
|
||
_set_status("Testlauf beendet — zurück im Editor (Auswahl und Dokument unverändert).")
|
||
# Trenner-Lagen zurücksetzen (Sly-Befund 20.10.): das Re-Layout nach dem
|
||
# Wiedereinblenden der Panels setzt die Trenner auf Defaults — die gemerkten
|
||
# Lagen deshalb deferred UND im nächsten Frame zurücksetzen (das Layout
|
||
# muss die Panels erst neu vermessen haben, sonst clampen sie erneut).
|
||
_playtest_split_lagen_zurueck.call_deferred()
|
||
get_tree().process_frame.connect(_playtest_split_lagen_zurueck, CONNECT_ONE_SHOT)
|
||
|
||
|
||
## Gemerkte Playtest-Trenner-Lagen zurück auf die Splitter schreiben.
|
||
func _playtest_split_lagen_zurueck() -> void:
|
||
if _split_lagen_playtest.is_empty():
|
||
return
|
||
for sname in _split_lagen_playtest.keys():
|
||
var sp := _find_split(str(sname))
|
||
if sp != null:
|
||
sp.split_offsets = _split_lagen_playtest[sname]
|
||
|
||
|
||
func _on_new_map_confirmed() -> void:
|
||
doc = SvmapDocument.create_new(new_dialog.get_map_name(), new_dialog.get_size_m(), new_dialog.get_weather())
|
||
current_path = ""
|
||
dirty = false
|
||
history.begin(doc)
|
||
_clear_edit_state()
|
||
_refresh_all()
|
||
_test_start_reload() # Karte t_96e3c4ee: neue Map → kein Test-Start gesetzt
|
||
_set_status("Neue Map „%s“ erstellt (%.0f × %.0f m, Wetter: %s). Wähle links ein Objekt, um zu bauen!" % [
|
||
new_dialog.get_map_name(), new_dialog.get_size_m(), new_dialog.get_size_m(), new_dialog.get_weather()])
|
||
|
||
|
||
func _on_open_selected(path: String) -> void:
|
||
var result := SvmapIO.load_from_file(path)
|
||
if not result["ok"]:
|
||
_show_errors("Map konnte NICHT geladen werden:\n(Falls sie in einem anderen Programm geändert wurde, prüfe die Datei.)", result["errors"])
|
||
_set_status("Laden fehlgeschlagen: %s — versuche eine .svmap-Datei aus dem Map-Editor oder vom Server." % path.get_file(), true)
|
||
return
|
||
doc = result["data"]
|
||
current_path = path
|
||
dirty = false
|
||
history.begin(doc)
|
||
_clear_edit_state()
|
||
_refresh_all()
|
||
_test_start_reload() # Karte t_96e3c4ee: Sidecar der geladenen Map lesen
|
||
var map_name := str(doc.get("info", {}).get("name", "?"))
|
||
_set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)])
|
||
|
||
|
||
func _clear_edit_state() -> void:
|
||
if interaction != null:
|
||
interaction.call("clear_selection", true)
|
||
interaction.call("cancel_placement")
|
||
|
||
|
||
func _save() -> void:
|
||
if current_path.is_empty():
|
||
save_dialog.popup_centered()
|
||
return
|
||
_save_to(current_path)
|
||
|
||
|
||
func _on_save_selected(path: String) -> void:
|
||
if not path.get_extension().is_empty() and path.get_extension() != "svmap":
|
||
path += ".svmap"
|
||
_save_to(path)
|
||
|
||
|
||
func _save_to(path: String) -> void:
|
||
# Felder stehen im Dokument (Live-Bindung) — vor dem Speichern noch einmal
|
||
# sicherstellen, dass Name/Autor nicht leer sind (verständliche Meldung statt V4-Kauderwelsch).
|
||
var name_v := str(doc.get("info", {}).get("name", "")).strip_edges()
|
||
if name_v.is_empty():
|
||
_show_errors("Map konnte nicht gespeichert werden:", ["Die Map braucht einen Namen — trag ihn rechts unter „Map-Eigenschaften“ ein."])
|
||
return
|
||
var result := SvmapIO.save_to_file(path, doc)
|
||
if not result["ok"]:
|
||
_show_errors("Map konnte nicht gespeichert werden (ungültige Map-Daten):", result["errors"])
|
||
_set_status("Speichern fehlgeschlagen.", true)
|
||
return
|
||
current_path = path
|
||
dirty = false
|
||
_test_start_reload() # Karte t_96e3c4ee: Punkt wandert beim Speichern mit
|
||
_refresh_file_label()
|
||
_set_status("Map gespeichert: %s" % path)
|
||
_refresh_tree()
|
||
|
||
|
||
func _show_errors(title: String, errors: Array) -> void:
|
||
var lines := ""
|
||
for e in errors:
|
||
lines += "• %s\n" % str(e)
|
||
error_text.text = "%s\n%s" % [title, lines]
|
||
error_dialog.title = "Fehler"
|
||
error_dialog.popup_centered()
|
||
|
||
|
||
# ------------------------------------------- Karte 3/3: Globale Shortcuts ---
|
||
|
||
## Verlust-Operation (Neue Map / Öffnen): bei ungespeicherten Änderungen erst
|
||
## NACHFRAGEN (deutscher Dialog), sonst direkt ausführen. Die wartende Aktion
|
||
## liegt in _pending_after_dirty und läuft bei „Verwerfen“ an.
|
||
func _request_lossy(next: Callable) -> void:
|
||
if ShortcutPolicy.needs_dirty_confirm(dirty):
|
||
_pending_after_dirty = next
|
||
dirty_dialog.popup_centered()
|
||
return
|
||
next.call()
|
||
|
||
|
||
## „Verwerfen“ im Dirty-Dialog: die wartende Aktion (Neu/Öffnen) jetzt ausführen.
|
||
func _on_dirty_confirmed() -> void:
|
||
var next := _pending_after_dirty
|
||
_pending_after_dirty = Callable()
|
||
if next.is_valid():
|
||
next.call()
|
||
|
||
|
||
## Karte 3/3: Dürfen die globalen Tasten (F1–F5, H) gerade wirken? Pure Policy
|
||
## in ShortcutPolicy — hier nur die Fakten: Textfeld/SpinBox fokussiert? Ein
|
||
## Dialog/Popup offen? (Eigene Fenster des Editors; das Hilfe-Panel ist
|
||
## absichtlich KEIN Fenster und zählt nicht.)
|
||
func _tool_key_allowed() -> bool:
|
||
var focus := get_viewport().gui_get_focus_owner()
|
||
var text_focused: bool = focus != null \
|
||
and (focus is LineEdit or focus is TextEdit or focus is SpinBox)
|
||
var dialog_open := false
|
||
for child in get_children():
|
||
if child is Window and child.visible:
|
||
dialog_open = true
|
||
break
|
||
return ShortcutPolicy.tool_keys_allowed(text_focused, dialog_open)
|
||
|
||
|
||
## Kurzhilfe-Overlay umschalten; Statuszeile meldet den Zustand.
|
||
func _toggle_help_overlay() -> void:
|
||
help_overlay.visible = not help_overlay.visible
|
||
if help_overlay.visible:
|
||
_set_status("Kurzhilfe an — H oder Esc schließt das Overlay wieder.")
|
||
else:
|
||
_base_tool_status()
|
||
|
||
|
||
## Permanente Werkzeug-Anzeige (Karte 3/3): aktives Werkzeug + Flug-Hint —
|
||
## Sly sieht in der Statuszeile immer, wo er ist.
|
||
func _update_tool_label() -> void:
|
||
if tool_label == null:
|
||
return
|
||
var names := {
|
||
"objects": "Objekte (F2)",
|
||
"terrain": "Terrain (F1)",
|
||
"bauen": "Bauen (F3)",
|
||
"spawns": "Spawn-Zonen (F4)",
|
||
}
|
||
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
|
||
|
||
|
||
## Symmetrie-Schalter (Karte t_f5935023, M1): Modus aus dem Toolbar-Menü übernehmen.
|
||
func _on_sym_mode_selected(index: int) -> void:
|
||
sym_mode = clampi(index, 0, SymmetryCore.MODE_LABELS.size() - 1)
|
||
_update_sym_ui()
|
||
if sym_mode == SymmetryCore.Mode.OFF:
|
||
_set_status("Symmetrie aus — die Karte wird nicht mehr automatisch gespiegelt.")
|
||
else:
|
||
_set_status("Symmetrie: %s — Änderungen an einer Seite werden auf die andere übertragen." %
|
||
SymmetryCore.mode_label(sym_mode))
|
||
|
||
|
||
## Symmetrie-Status: Toolbar-Text + Haken + Statuszeilen-Label (M1 Fundament).
|
||
## Das tatsächliche Spiegeln von Änderungen kommt mit M2–M4.
|
||
func _update_sym_ui() -> void:
|
||
if sym_menu == null or sym_label == null:
|
||
return
|
||
var label := "Symmetrie: %s" % SymmetryCore.mode_label(sym_mode)
|
||
sym_menu.text = label
|
||
sym_label.text = label
|
||
var popup := sym_menu.get_popup()
|
||
for i in SymmetryCore.MODE_LABELS.size():
|
||
popup.set_item_checked(i, i == sym_mode)
|
||
|
||
|
||
# --------------------------------------------------------------- History ----
|
||
|
||
func _do_undo() -> void:
|
||
if not history.can_undo():
|
||
_set_status("Nichts zum Rückgängigmachen — die History ist schon am Anfang.")
|
||
return
|
||
var desc := history.undo()
|
||
_after_structure_change()
|
||
_set_status("Rückgängig gemacht: %s. Mit Strg+Y kannst du es wiederherstellen." % desc)
|
||
|
||
|
||
func _do_redo() -> void:
|
||
if not history.can_redo():
|
||
_set_status("Nichts zum Wiederholen — schon am aktuellsten Stand.")
|
||
return
|
||
var desc := history.redo()
|
||
_after_structure_change()
|
||
_set_status("Wiederhergestellt: %s." % desc)
|
||
|
||
|
||
## F: Auswahl ins Bild zentrieren (Karte t_c46d31c2) — Kamera-Ziel auf den
|
||
## Auswahl-Mittelpunkt; im Flugmodus dreht sich nur die Blickrichtung.
|
||
func _focus_selection() -> void:
|
||
var indices: Array = interaction.selected_indices
|
||
if indices.is_empty():
|
||
_set_status("Nichts ausgewählt — F zentriert die Auswahl im Bild. Klicke zuerst ein Objekt an (im Viewport oder im Baum).")
|
||
return
|
||
var center := SelectionMath.focus_center(view_builder.object_markers, indices)
|
||
camera.call("focus_on", center)
|
||
_set_status("%d Objekt(e) ins Bild zentriert — Strg+Klick/Strg+LMB markiert weitere dazu, Entf löscht die ganze Auswahl." % indices.size())
|
||
|
||
|
||
## Karte t_2470de89: Der alte Rechtsklick-Filter (_fly_rmb_blocked) ist weg —
|
||
## RMB startet den Freelook-Flug in JEDEM Werkzeugzustand (absoluter Vorrang).
|
||
|
||
|
||
func _refresh_history_buttons() -> void:
|
||
undo_btn.disabled = not history.can_undo()
|
||
redo_btn.disabled = not history.can_redo()
|
||
undo_btn.tooltip_text = ("Rückgängig machen: %s (Strg+Z)" % history.undo_desc()) if history.can_undo() else "Nichts zum Rückgängigmachen"
|
||
redo_btn.tooltip_text = ("Wiederherstellen: %s (Strg+Y)" % history.redo_desc()) if history.can_redo() else "Nichts zum Wiederholen"
|
||
|
||
|
||
func _unhandled_key_input(event: InputEvent) -> void:
|
||
if not (event is InputEventKey) or not event.is_pressed() or event.is_echo():
|
||
return
|
||
var key_evt := event as InputEventKey
|
||
var key := key_evt.keycode
|
||
# InputEventKey hat KEIN `command_pressed` (Godot-3-API-Relikt — crashete
|
||
# bei jeder Taste, GUI-Start-Befund 15.10.). Die Godot-4-Ersatz-API ist
|
||
# is_command_or_control_pressed(): Ctrl auf Linux/Windows, Cmd auf macOS.
|
||
var ctrl: bool = key_evt.is_command_or_control_pressed()
|
||
# Lauf-Modus (t_1cdc1a72): nur ESC behandeln — ALLE anderen Editor-Tasten
|
||
# (F1-F5, Undo/Redo, Werkzeuge) sind im Spiel eingefroren.
|
||
if playtest != null and playtest.active:
|
||
if key == KEY_ESCAPE:
|
||
playtest.handle_escape()
|
||
get_viewport().set_input_as_handled()
|
||
elif key == KEY_T:
|
||
# Karte t_96e3c4ee, „jederzeit": T im Testlauf = Test-Start auf den
|
||
# Punkt unterm Fadenkreuz setzen + SOFORT dorthin versetzen
|
||
# (Augenhöhe 1,70 m, Kollision geprüft).
|
||
var tp: Dictionary = playtest.call("teleport_crosshair")
|
||
if bool(tp.get("ok")):
|
||
_test_start_apply(float(tp.get("x")), float(tp.get("z")), str(tp.get("hinweis")))
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
match key:
|
||
KEY_T:
|
||
# Karte t_96e3c4ee: T schaltet das Test-Start-Werkzeug um (Editor).
|
||
if not ctrl:
|
||
_set_tool_mode("teststart" if tool_mode != "teststart" else "objects")
|
||
get_viewport().set_input_as_handled()
|
||
KEY_Z:
|
||
if ctrl:
|
||
if event.shift_pressed:
|
||
_do_redo()
|
||
else:
|
||
_do_undo()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_Y:
|
||
if ctrl:
|
||
_do_redo()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_S:
|
||
if ctrl:
|
||
if event.shift_pressed:
|
||
# Strg+Umschalt+S (Karte 3/3): Speichern unter …
|
||
save_dialog.popup_centered()
|
||
else:
|
||
_save()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_O:
|
||
if ctrl:
|
||
# Strg+O (Karte 3/3): Map öffnen — bei dirty erst NACHFRAGEN.
|
||
_request_lossy(func() -> void: open_dialog.popup_centered())
|
||
get_viewport().set_input_as_handled()
|
||
KEY_N:
|
||
if ctrl:
|
||
# Strg+N (Karte 3/3): Neue Map — bei dirty erst NACHFRAGEN.
|
||
_request_lossy(func() -> void: new_dialog.popup_centered())
|
||
get_viewport().set_input_as_handled()
|
||
KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5:
|
||
# Karte 3/3: Werkzeugwechsel + Test-Platzhalter. NUR wirken, wenn
|
||
# kein Textfeld tippt und kein Dialog offen ist (ShortcutPolicy).
|
||
if _tool_key_allowed():
|
||
match key:
|
||
KEY_F1:
|
||
_set_tool_mode("terrain")
|
||
KEY_F2:
|
||
_set_tool_mode("objects")
|
||
KEY_F3:
|
||
_set_tool_mode("bauen")
|
||
KEY_F4:
|
||
_set_tool_mode("spawns")
|
||
KEY_F5:
|
||
# ▶ Karte testen: der echte Freigabe-/Test-Flow existiert
|
||
# (Karte t_b84af40f) — F5 ruft ihn direkt auf.
|
||
_on_test_run()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_H:
|
||
# Karte 3/3: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls).
|
||
if _tool_key_allowed():
|
||
_toggle_help_overlay()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_L:
|
||
# Karte t_89c0ec95: Höhenlinien FIXIEREN an/aus — unabhängig vom
|
||
# Werkzeug (wer die Linien dauerhaft will, fixiert sie hier).
|
||
# Ohne Modifier (kein Strg+L), gleiche Freigabe wie F1–F5/H:
|
||
# Tippen in Textfeldern und offene Dialoge gehen vor.
|
||
if not ctrl and _tool_key_allowed():
|
||
_on_contours_toggled(not _contours_on)
|
||
get_viewport().set_input_as_handled()
|
||
KEY_G:
|
||
if ctrl:
|
||
# Gruppieren/Lösen (Karte t_f6f29d86)
|
||
if event.shift_pressed:
|
||
interaction.ungroup_selection()
|
||
else:
|
||
interaction.group_selection()
|
||
get_viewport().set_input_as_handled()
|
||
elif tool_mode == "objects" and not interaction.placing:
|
||
# G = Greifen (Karte t_55257792, Blender-Semantik): Auswahl
|
||
# hängt an der Maus, Linksklick setzt neu, Esc bricht ab.
|
||
interaction.grab_begin()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_R:
|
||
if not ctrl:
|
||
# R = Vierteldrehung 90° (Karte t_55257792): Bauen-Modus dreht
|
||
# Vorschau/Auswahl (wie bisher), Objekt-Auswahl UND Ghost drehen
|
||
# jetzt auch in 90°-Schritten (Umschalt+R = zurück). Feindrehen
|
||
# 15° bleibt am Mausrad.
|
||
var r_delta := -90.0 if event.shift_pressed else 90.0
|
||
if tool_mode == "bauen":
|
||
build_tool.call("rotate_input")
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
if interaction.placing:
|
||
interaction.place_rot_deg = wrapf(interaction.place_rot_deg + r_delta, 0.0, 360.0)
|
||
else:
|
||
interaction.rotate_selected(r_delta)
|
||
get_viewport().set_input_as_handled()
|
||
KEY_F:
|
||
if not ctrl:
|
||
if event.shift_pressed:
|
||
# Umschalt+F: Freelook-Flug dauerhaft an/aus (Karte t_c46d31c2).
|
||
camera.call("toggle_fly_lock")
|
||
get_viewport().set_input_as_handled()
|
||
else:
|
||
# F: Auswahl ins Bild zentrieren.
|
||
_focus_selection()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_ESCAPE:
|
||
# Karte 3/3: Steht das Hilfe-Overlay offen, gehört Esc IHM (schließen)
|
||
# — VOR der normalen Esc-Prioritätskette.
|
||
if help_overlay.visible:
|
||
_toggle_help_overlay()
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
# Karte t_e0120dc2: Läuft gerade ein Gizmo-Zug (Achsen-Pfeil), gehört
|
||
# Esc IHM — exakt zurückrollen auf die Zug-Startposition (kein
|
||
# History-Eintrag), danach NICHT weiter in der Esc-Kette eintauchen.
|
||
if gizmo != null and gizmo.cancel_drag_if_active():
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
|
||
# Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
|
||
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
|
||
var esc := ShortcutPolicy.esc_action(tool_mode,
|
||
interaction.placing,
|
||
interaction.selected_index != -1 or not interaction.selected_indices.is_empty(),
|
||
bool(interaction.get("_grabbing")))
|
||
match esc:
|
||
"exit_terrain":
|
||
_set_tool_mode("objects")
|
||
_set_status("Terrain-Modus beendet — zurück zum Objekt-Werkzeug.")
|
||
"exit_spawns":
|
||
_set_tool_mode("objects")
|
||
_set_status("Spawn-Werkzeug beendet — zurück zum Objekt-Werkzeug.")
|
||
"exit_bauen":
|
||
_set_tool_mode("objects")
|
||
_set_status("Bauen-Modus beendet — zurück zum Objekt-Werkzeug.")
|
||
"exit_teststart":
|
||
# Der gesetzte Test-Start bleibt bestehen (nur Werkzeug aus).
|
||
_set_tool_mode("objects")
|
||
_set_status("Test-Start-Werkzeug beendet — zurück zum Objekt-Werkzeug.")
|
||
"cancel_grab":
|
||
interaction.grab_cancel()
|
||
"cancel_placement":
|
||
interaction.cancel_placement()
|
||
_set_status("Platzieren abgebrochen.")
|
||
"clear_selection":
|
||
interaction.clear_selection()
|
||
_set_status("Auswahl aufgehoben.")
|
||
if esc != "none":
|
||
get_viewport().set_input_as_handled()
|
||
KEY_DELETE:
|
||
if not ctrl:
|
||
if interaction.placing:
|
||
return
|
||
if tool_mode == "spawns":
|
||
spawn_tool.call("delete_selected")
|
||
elif tool_mode == "bauen":
|
||
build_tool.call("delete_selected")
|
||
elif tool_mode == "teststart":
|
||
# Karte t_96e3c4ee: Entf im Test-Start-Werkzeug = Punkt löschen.
|
||
_test_start_delete()
|
||
else:
|
||
interaction.delete_selected()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_D:
|
||
if ctrl and tool_mode == "objects":
|
||
interaction.duplicate_selected()
|
||
get_viewport().set_input_as_handled()
|
||
KEY_PAGEUP:
|
||
if interaction.placing:
|
||
return
|
||
if ctrl:
|
||
# Strg+Bild↑ (Karte t_55257792): Auswahl/Bauteil auf den Boden.
|
||
if tool_mode == "bauen":
|
||
build_tool.call("snap_selected_to_floor")
|
||
get_viewport().set_input_as_handled()
|
||
elif tool_mode == "objects" and interaction.selected_index >= 0:
|
||
interaction.snap_selection_to_floor()
|
||
get_viewport().set_input_as_handled()
|
||
elif not ctrl:
|
||
if tool_mode == "bauen":
|
||
build_tool.call("lift_selected", ObjectOps.LIFT_STEP_M)
|
||
get_viewport().set_input_as_handled()
|
||
elif tool_mode == "objects" and interaction.selected_index >= 0:
|
||
interaction.lift_selected(ObjectOps.LIFT_STEP_M)
|
||
get_viewport().set_input_as_handled()
|
||
KEY_PAGEDOWN:
|
||
if not ctrl and not interaction.placing:
|
||
if tool_mode == "bauen":
|
||
build_tool.call("lift_selected", -ObjectOps.LIFT_STEP_M)
|
||
get_viewport().set_input_as_handled()
|
||
elif tool_mode == "objects" and interaction.selected_index >= 0:
|
||
interaction.lift_selected(-ObjectOps.LIFT_STEP_M)
|
||
get_viewport().set_input_as_handled()
|
||
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT:
|
||
# Pfeiltasten (Karte t_55257792): Auswahl im 2-m-Raster verschieben,
|
||
# Richtung = Kamera-Ausrichtung nächste Weltachse (MoveMath). Eine
|
||
# Zelle pro Tastendruck; beim Platzieren bleibt der Ghost herrschend.
|
||
if interaction.placing:
|
||
return
|
||
var fwd := 0
|
||
var side := 0
|
||
match key:
|
||
KEY_UP:
|
||
fwd = 1
|
||
KEY_DOWN:
|
||
fwd = -1
|
||
KEY_RIGHT:
|
||
side = 1
|
||
KEY_LEFT:
|
||
side = -1
|
||
var delta := MoveMath.nudge_delta(
|
||
float(camera.call("view_yaw_deg")), fwd, side)
|
||
if tool_mode == "bauen":
|
||
build_tool.call("nudge_selected", delta)
|
||
get_viewport().set_input_as_handled()
|
||
elif tool_mode == "objects" and interaction.selected_index >= 0:
|
||
interaction.nudge_selection(delta)
|
||
get_viewport().set_input_as_handled()
|
||
KEY_BRACKETLEFT, KEY_BRACKETRIGHT:
|
||
# Terrain-Pinsel [ / ] (Karte t_55257792): Größe durch die 3 Presets
|
||
# zyklisch — nur im Terrain-Werkzeug (Ziffern-Freiheit, Punkt 6).
|
||
if tool_mode == "terrain":
|
||
_terrain_brush_size_step(key == KEY_BRACKETRIGHT)
|
||
get_viewport().set_input_as_handled()
|
||
KEY_1, KEY_2, KEY_3, KEY_4:
|
||
# Terrain-Pinsel-Modi 1–4 (Karte t_55257792): Heben/Senken/Glätten/
|
||
# Malen — NUR im Terrain-Werkzeug (Konfliktfreiheit, Punkt 6; im
|
||
# Objekt-/Bauen-Modus bleiben die Ziffern unbenutzt).
|
||
if tool_mode == "terrain":
|
||
_terrain_brush_mode_key(int(key) - int(KEY_1))
|
||
get_viewport().set_input_as_handled()
|
||
|
||
|
||
# --------------------------------------------------------- Bibliothek ------
|
||
# U1: die Bibliothek wohnt im Assets-Bereich unten (assets_panel.gd); die
|
||
# Tools rufen weiter main.call("_highlight_library") — dünne Weiterleitung.
|
||
|
||
func _on_library_pressed(oid: String) -> void:
|
||
assets_mod._on_library_pressed(oid)
|
||
|
||
|
||
## U1-Delegation: die Bibliotheks-Logik wohnt in assets_panel.gd — die alten
|
||
## main.-Aufrufe (Werkzeuge/Tests) bleiben als dünne Delegaten erhalten.
|
||
func _apply_library_filter(query: String) -> void:
|
||
assets_mod._apply_library_filter(query)
|
||
|
||
|
||
func _on_library_expand_all() -> void:
|
||
assets_mod._on_library_expand_all()
|
||
|
||
|
||
func _on_library_collapse_all() -> void:
|
||
assets_mod._on_library_collapse_all()
|
||
|
||
|
||
## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf.
|
||
func _highlight_library(oid: String) -> void:
|
||
assets_mod.highlight_library(oid)
|
||
|
||
|
||
func object_label(oid: String) -> String:
|
||
return LibraryBrowser.object_label(oid)
|
||
|
||
|
||
## Snap to Ground (1 Klick): ALLE Objekte auf die Terrain-Höhe setzen —
|
||
## EIN Undo-Eintrag für die ganze Aktion.
|
||
func _on_snap_to_ground() -> void:
|
||
var objects: Array = doc.get("objects", [])
|
||
if objects.is_empty():
|
||
_set_status("Es gibt noch keine Objekte — platziere zuerst eins aus der Bibliothek links.")
|
||
return
|
||
var before: Dictionary = history.snapshot(["objects"])
|
||
var changed := ObjectOps.snap_to_ground(doc)
|
||
if changed == 0:
|
||
_set_status("Alle %d Objekte stehen schon auf dem Boden — nichts zu tun." % objects.size())
|
||
return
|
||
history.commit("Alle Objekte auf Boden gesetzt (%d)" % changed,
|
||
before, history.snapshot(["objects"]))
|
||
_after_structure_change()
|
||
_set_status("%d von %d Objekten auf den Boden gesetzt — Strg+Z nimmt es zurück." % [changed, objects.size()])
|
||
|
||
|
||
func _on_grid_snap_changed(pressed: bool) -> void:
|
||
grid_snap = pressed
|
||
_set_status("Raster-Snap %s — Objekte landen jetzt %s." % [
|
||
"AN" if pressed else "AUS",
|
||
"auf ganzen Metern (wie Bauklötze nebeneinander)" if pressed else "dort, wo du hinklickst"])
|
||
|
||
|
||
# ---------------------------------------------------- Werkzeug-Status ------
|
||
|
||
## Standard-Hilfstext, wenn gerade nichts "Im Gange" ist.
|
||
func _base_tool_status() -> void:
|
||
_set_status("Bereit. Links ein Objekt anklicken zum Platzieren oder in der Karte ein Objekt auswählen.")
|
||
|
||
|
||
# ------------------------------------------------------ Terrain-Werkzeug ----
|
||
# (Karte t_9aba04f1 — Höhenfeld heben/senken/glätten)
|
||
|
||
## U1-Delegation: die Widgets wohnen in den Modulen (terrain_panel.gd,
|
||
## check_panel.gd); der Koordinator hält die Werkzeug-Logik und steuert die
|
||
## Modul-Widgets über deren öffentliche Felder. Methoden, die die Tools per
|
||
## main.call(...) erreichen (_on_terrain_stroke_committed, _sync_spawn_selection,
|
||
## _refresh_spawn_status, _mark_tree_selection…), bleiben unter denselben
|
||
## Namen hier (dünne Weiterleitung) — der Werkzeug-Vertrag ist unverändert.
|
||
|
||
func _on_terrain_btn_pressed() -> void:
|
||
if terrain_mod.terrain_edit_btn.button_pressed:
|
||
_set_tool_mode("terrain")
|
||
else:
|
||
_set_tool_mode("objects")
|
||
|
||
|
||
## Werkzeug-Modus wechseln: "objects" (Platzieren/Auswahl), "terrain" (Pinsel),
|
||
## "spawns" (Spawn-Zonen malen).
|
||
func _set_tool_mode(mode: String) -> void:
|
||
if mode == tool_mode:
|
||
_sync_terrain_button()
|
||
_sync_spawn_button()
|
||
_sync_build_button()
|
||
_sync_test_start_button()
|
||
_update_tool_label()
|
||
return
|
||
if mode == "terrain":
|
||
_leave_spawn_mode()
|
||
_leave_build_mode()
|
||
_leave_test_start_mode()
|
||
_enter_terrain_mode()
|
||
elif mode == "spawns":
|
||
_leave_terrain_mode()
|
||
_leave_build_mode()
|
||
_leave_test_start_mode()
|
||
_enter_spawn_mode()
|
||
elif mode == "bauen":
|
||
_leave_terrain_mode()
|
||
_leave_spawn_mode()
|
||
_leave_test_start_mode()
|
||
_enter_build_mode()
|
||
elif mode == "teststart":
|
||
_leave_terrain_mode()
|
||
_leave_spawn_mode()
|
||
_leave_build_mode()
|
||
_enter_test_start_mode()
|
||
else:
|
||
_leave_terrain_mode()
|
||
_leave_spawn_mode()
|
||
_leave_build_mode()
|
||
_leave_test_start_mode()
|
||
_enter_objects_mode()
|
||
_update_tool_label()
|
||
# Karte t_89c0ec95: Werkzeugwechsel = Höhenlinien-Umschalter — nach dem
|
||
# Terrain-Werkzeug (F1) wieder aus, bei Terrain-Eintritt wieder an
|
||
# (sofern nicht fixiert). Signal-getrieben an diesem EINEN Wechselpunkt,
|
||
# kein per-Frame-Poll.
|
||
_sync_contours_to_builder()
|
||
|
||
|
||
func _enter_terrain_mode() -> void:
|
||
interaction.call("cancel_placement")
|
||
interaction.call("clear_selection", true)
|
||
if not terrain_tool.call("activate"):
|
||
# Abgelehnt (z. B. Auflösungs-Limit) — Button zurück, Meldung steht.
|
||
_sync_terrain_button()
|
||
return
|
||
tool_mode = "terrain"
|
||
terrain_mod.terrain_panel.visible = true
|
||
help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Strg+Linksklick: Gegenteil / Gras zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Rechte Maustaste: Flug · Strg+Z: letzter Strich weg"
|
||
terrain_mod.terrain_edit_btn.button_pressed = true
|
||
terrain_tool.call("set_brush_mode", terrain_mod._selected_terrain_mode())
|
||
_set_status("Terrain-Modus an: LINKSKLICK-Ziehen formt den Boden, im Malen-Modus färbt er das Gelände (Gras/Sand/Fels). Regler rechts stellen Pinsel und Stärke ein.")
|
||
|
||
|
||
func _leave_terrain_mode() -> void:
|
||
if terrain_tool != null:
|
||
terrain_tool.call("deactivate")
|
||
if terrain_mod != null:
|
||
terrain_mod.terrain_panel.visible = false
|
||
_sync_terrain_button()
|
||
|
||
|
||
func _enter_spawn_mode() -> void:
|
||
interaction.call("cancel_placement")
|
||
interaction.call("clear_selection", true)
|
||
tool_mode = "spawns"
|
||
check_mod.spawn_panel.visible = true
|
||
help_label.text = "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nTeam/Radius rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
||
check_mod.spawn_edit_btn.button_pressed = true
|
||
spawn_tool.call("activate")
|
||
spawn_tool.call("set_team", check_mod._selected_spawn_team())
|
||
spawn_tool.call("set_radius", float(check_mod.spawn_radius_slider.value))
|
||
_refresh_spawn_status()
|
||
_set_status("Spawn-Modus an: Klicke auf den Boden, um Zonen für %s zu malen. Die Kreis-Vorschau folgt der Maus." % SpawnOps.team_label(check_mod._selected_spawn_team()))
|
||
|
||
|
||
func _leave_spawn_mode() -> void:
|
||
if spawn_tool != null:
|
||
spawn_tool.call("deactivate")
|
||
if check_mod != null:
|
||
check_mod.spawn_panel.visible = false
|
||
_sync_spawn_button()
|
||
|
||
|
||
func _enter_objects_mode() -> void:
|
||
tool_mode = "objects"
|
||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||
_base_tool_status()
|
||
|
||
|
||
# ------------------------------------------------------ Bauen-Werkzeug ------
|
||
# (Karte t_97ea59c5 — Modular-Builder-Pieces direkt im App-Skelett; die
|
||
# Godot-Editor-UI des Plugins bleibt unangetastet im Addon aktivierbar.)
|
||
# U1: die Bauteil-Widgets wohnen im Terrain-Tab (terrain_panel.gd, Werkzeug-
|
||
# Zusammenhang); die Werkzeug-Logik bleibt hier im Koordinator. Tests/Tools
|
||
# erreichen sie über main.build_edit_btn/main.build_kind_buttons wie vorher.
|
||
|
||
|
||
func _on_build_btn_pressed() -> void:
|
||
if build_edit_btn.button_pressed:
|
||
_set_tool_mode("bauen")
|
||
else:
|
||
_set_tool_mode("objects")
|
||
|
||
|
||
func _on_build_kind_pressed(kind_id: String) -> void:
|
||
if tool_mode != "bauen":
|
||
_set_tool_mode("bauen")
|
||
for other in build_kind_buttons:
|
||
(build_kind_buttons[other] as Button).button_pressed = false
|
||
(build_kind_buttons[kind_id] as Button).button_pressed = true
|
||
build_tool.call("set_kind", kind_id)
|
||
|
||
|
||
func _enter_build_mode() -> void:
|
||
interaction.call("cancel_placement")
|
||
interaction.call("clear_selection", true)
|
||
tool_mode = "bauen"
|
||
build_panel.visible = true
|
||
help_label.text = "Linksklick: Bauteil setzen (auch auf andere Teile = stapeln) · Umschalt+Klick: Bauteil auswählen ·\nR: drehen (90°) · Pfeiltasten: im 2-m-Raster verschieben · Bild↑/↓: anheben/absenken · Strg+Bild↑: auf Boden · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
||
build_edit_btn.button_pressed = true
|
||
build_tool.call("activate")
|
||
_set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.")
|
||
|
||
|
||
func _leave_build_mode() -> void:
|
||
if build_tool != null:
|
||
build_tool.call("deactivate")
|
||
if build_panel != null:
|
||
build_panel.visible = false
|
||
_sync_build_button()
|
||
|
||
|
||
func _sync_build_button() -> void:
|
||
build_edit_btn.button_pressed = tool_mode == "bauen"
|
||
|
||
|
||
func _sync_terrain_button() -> void:
|
||
terrain_mod.terrain_edit_btn.button_pressed = tool_mode == "terrain"
|
||
|
||
|
||
func _terrain_brush_size_step(up: bool) -> void:
|
||
terrain_mod.terrain_brush_size_step(up)
|
||
|
||
|
||
func _terrain_brush_mode_key(mode_idx: int) -> void:
|
||
var t: Control = terrain_mod
|
||
var btn: Button = null
|
||
match mode_idx:
|
||
0:
|
||
btn = t.terrain_mode_raise
|
||
1:
|
||
btn = t.terrain_mode_lower
|
||
2:
|
||
btn = t.terrain_mode_smooth
|
||
3:
|
||
btn = t.terrain_mode_paint
|
||
if btn == null or terrain_tool == null:
|
||
return
|
||
btn.set_pressed(true)
|
||
var labels := ["Heben", "Senken", "Glätten", "Malen"]
|
||
var extra := ""
|
||
if mode_idx == 3:
|
||
var mats: Array = SvmapIO.MATERIAL_IDS
|
||
extra = " (Material: %s — mit der Palette rechts wechseln)" % str(mats[clampi(int(terrain_tool.get("paint_material")), 0, mats.size() - 1)])
|
||
_set_status("Pinsel-Modus: %s%s — Linksklick-Ziehen formt/malt jetzt so (Ziffern 1–4 wechseln)." % [str(labels[mode_idx]), extra])
|
||
|
||
|
||
## Ein Pinsel-Strich ist fertig (Doc + History schon aktualisiert vom Tool).
|
||
func _on_terrain_stroke_committed() -> void:
|
||
dirty = true
|
||
_refresh_file_label()
|
||
_refresh_stats()
|
||
_refresh_history_buttons()
|
||
view_builder.call("rebuild_terrain_from_doc", doc)
|
||
var desc: String = history.undo_desc()
|
||
_set_status("%s — Strg+Z macht den ganzen Strich rückgängig." % desc)
|
||
|
||
|
||
## Terrain-Modus nach Laden/Neu/Undo mit dem Doc synchron halten (Cache frisch,
|
||
## Auflösungs-Limit prüfen — sonst dezenter Rückschalter).
|
||
func _recheck_terrain_mode() -> void:
|
||
if tool_mode != "terrain" or terrain_tool == null:
|
||
return
|
||
if not terrain_tool.call("refresh_from_doc"):
|
||
terrain_tool.call("deactivate")
|
||
tool_mode = "objects"
|
||
terrain_mod.terrain_panel.visible = false
|
||
_sync_terrain_button()
|
||
_update_tool_label()
|
||
# Karte t_89c0ec95: Dezenter Terrain-Rückschalter = ebenfalls ein
|
||
# Werkzeugwechsel — Linien wieder ausblenden (sofern nicht fixiert).
|
||
_sync_contours_to_builder()
|
||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||
_set_status("Terrain-Modus aus: Diese Map passt nicht ins Editor-MVP (Terrain-Auflösung über %d) — die Objekte bleiben normal bearbeitbar." % TerrainField.EDITOR_MAX_RESOLUTION, true)
|
||
|
||
|
||
# ---------------------------------------------------- Spawn-Zonen-Werkzeug ---
|
||
|
||
func _on_spawn_btn_pressed() -> void:
|
||
check_mod._on_spawn_btn_pressed()
|
||
|
||
|
||
func _sync_spawn_button() -> void:
|
||
check_mod.spawn_edit_btn.button_pressed = tool_mode == "spawns"
|
||
|
||
|
||
## Panel-Zeile „Welche Zone ist angewählt?“ — WIRD vom Spawn-Tool gerufen.
|
||
func _sync_spawn_selection() -> void:
|
||
check_mod.sync_spawn_selection()
|
||
|
||
|
||
## LIVE-Validierung (Loader-Regeln) — immer sichtbar, nach JEDEM Struktur-Wechsel.
|
||
func _refresh_spawn_status() -> void:
|
||
check_mod.refresh_spawn_status()
|
||
|
||
|
||
## Map-Fertig-Check (Export-Button-Aktion): Klartext-Liste + Shake, wenn fehlt.
|
||
func _on_check_map() -> void:
|
||
var report := SpawnOps.ready_report(doc, manifest)
|
||
var lines := ""
|
||
for line in report["lines"]:
|
||
lines += "%s\n" % str(line["text"])
|
||
var hint := str(line.get("hint", ""))
|
||
if hint != "":
|
||
lines += " → %s\n" % hint
|
||
check_text.text = lines
|
||
check_dialog.popup_centered()
|
||
if report["ok"]:
|
||
_set_status("✅ Karte prüfen: Alles bereit zum Export! Speichern erzeugt die signierte .svmap-Datei.")
|
||
else:
|
||
_shake_button(_registry_toolbar_button("karte_pruefen"))
|
||
_set_status("Map ist noch nicht bereit — siehe Warnungen im Map-Check-Fenster.", true)
|
||
|
||
|
||
## Kurzes Wackeln des Buttons — AUFMERKSAMKEIT statt Fehlerbombe (Chef-Grundsatz).
|
||
func _shake_button(btn: Control) -> void:
|
||
var orig: Vector2 = btn.position
|
||
var tween := create_tween()
|
||
for offset in [-9.0, 9.0, -6.0, 6.0, -3.0, 0.0]:
|
||
tween.tween_property(btn, "position", orig + Vector2(offset, 0.0), 0.045)
|
||
|
||
|
||
## Toolbar-Button per Registry-ID finden (karte_pruefen / karte_testen …).
|
||
func _registry_toolbar_button(id: String) -> Control:
|
||
var found: Array = toolbar_mod.find_children(id, "Button", true, false)
|
||
return found[0] if not found.is_empty() else null
|
||
|
||
|
||
## Baum-Rückrufe wohnen jetzt im Modul (src/ui/panels/scene_tree_panel.gd);
|
||
## die Tools rufen weiter main.call("_mark_tree_selection…") — dünne
|
||
## Weiterleitungen unten (Registry-Koordinator).
|
||
# ------------------------------------------------------- Struktur-Änderung --
|
||
|
||
## Nach undo/redo/platzieren/löschen: Struktur hat sich geändert → Ansicht neu.
|
||
func _after_structure_change() -> void:
|
||
dirty = true
|
||
_refresh_file_label()
|
||
_refresh_stats()
|
||
_refresh_history_buttons()
|
||
_refresh_spawn_status()
|
||
if interaction != null:
|
||
interaction.call("clear_selection", true)
|
||
view_builder.call("rebuild", doc, manifest)
|
||
_sync_grid_to_builder()
|
||
# Karte t_89c0ec95: der Rebuild baut terrain_view NEU (Default-Pref „an“)
|
||
# — auch die Höhenlinien brauchen ihren Nacheich (Gitter-Muster). Sonst
|
||
# kehrte z. B. nach Strg+Z die Dauersicht zurück, die Sly störte.
|
||
_sync_contours_to_builder()
|
||
_refresh_tree()
|
||
_recheck_terrain_mode()
|
||
if interaction != null:
|
||
interaction.call("refresh_terrain_cache") # t_4092e3a0: Objekt-Cursor trifft das Gelände
|
||
if tool_mode == "spawns" and spawn_tool != null:
|
||
spawn_tool.call("refresh_from_doc")
|
||
if tool_mode == "bauen" and build_tool != null:
|
||
build_tool.call("refresh_from_doc")
|
||
|
||
# --------------------------------------------------------------- Anzeige ---
|
||
|
||
func _refresh_all() -> void:
|
||
_loading_ui_values = true
|
||
map_mod.refresh_values()
|
||
_loading_ui_values = false
|
||
|
||
_refresh_tree()
|
||
_refresh_stats()
|
||
_refresh_file_label()
|
||
_refresh_history_buttons()
|
||
_refresh_viewport()
|
||
_refresh_spawn_status()
|
||
_recheck_terrain_mode()
|
||
|
||
|
||
func _refresh_viewport() -> void:
|
||
view_builder.call("rebuild", doc, manifest)
|
||
_sync_grid_to_builder()
|
||
_sync_contours_to_builder()
|
||
# Karte K1b (t_eca890c5): Laden/Neu = Ansicht auf den Karten-INHALT
|
||
# (Objekte + Bauteile + Spawnpunkte; Terrain zählt nicht) — ohne Inhalt
|
||
# wie bisher die ganze Karte. Pos1 (Home) bleibt ganze Karte.
|
||
var fb: Dictionary = doc.get("bounds", {})
|
||
var fmn: Array = fb.get("min", [])
|
||
var fmx: Array = fb.get("max", [])
|
||
if fmn.size() == 3 and fmx.size() == 3:
|
||
camera.call("fit_to_document", doc,
|
||
Vector3(fmn[0], fmn[1], fmn[2]), Vector3(fmx[0], fmx[1], fmx[2]))
|
||
|
||
|
||
# --- Maßstabsgitter (Karte t_7ac70014) -------------------------------------
|
||
|
||
func _on_grid_toggled(pressed: bool) -> void:
|
||
_grid_on = pressed
|
||
_sync_grid_to_builder()
|
||
|
||
|
||
## Nach jedem Viewport-Rebuild: Nutzer-Wahl (bzw. Boden-Referenz ohne Terrain-
|
||
## Block) wieder anwenden — der Rebuild setzt das Gitter auf seinen Default.
|
||
func _sync_grid_to_builder() -> void:
|
||
if view_builder == null:
|
||
return
|
||
var no_terrain: bool = doc.get("terrain", {}).is_empty()
|
||
view_builder.call("set_grid_visible", _grid_on or no_terrain)
|
||
if grid_btn != null and grid_btn.button_pressed != _grid_on:
|
||
grid_btn.set_pressed_no_signal(_grid_on)
|
||
|
||
|
||
# --- Höhenlinien (Karte t_3ebe4086, Werkzeug-Kopplung t_89c0ec95) ------------
|
||
|
||
## Effektive Höhenlinien-Sichtbarkeit: FIX-Anforderung des Nutzers (L/Button)
|
||
## ODER das Terrain-Werkzeug (F1) ist aktiv. Bewusst KEIN _process-Poll —
|
||
## der Hook hängt am Werkzeug-Wechselpunkt (_set_tool_mode) und am Button/L.
|
||
func _contours_effective() -> bool:
|
||
return _contours_on or tool_mode == "terrain"
|
||
|
||
|
||
func _on_contours_toggled(pressed: bool) -> void:
|
||
_contours_on = pressed
|
||
_sync_contours_to_builder()
|
||
if status_label != null:
|
||
_set_status("Höhenlinien %s — %s" % [
|
||
"FIXIERT AN" if _contours_on else "Fix aus",
|
||
"jetzt in jedem Werkzeug sichtbar (L oder Button schaltet zurück)."
|
||
if _contours_on else
|
||
"sichtbar nur noch im Terrain-Werkzeug (F1). L fixiert sie wieder."])
|
||
|
||
|
||
## Nach jedem Viewport-Rebuild und nach JEDEM Werkzeug-Wechsel: die effektive
|
||
## Wahl anwenden — der Rebuild baut das Terrain-Mesh (und damit dessen
|
||
## Shader-Uniform) neu, der Werkzeugwechsel blendet die Linien ein/aus.
|
||
func _sync_contours_to_builder() -> void:
|
||
if view_builder == null:
|
||
return
|
||
var terrain_view = view_builder.get("terrain_view")
|
||
if terrain_view != null and is_instance_valid(terrain_view):
|
||
terrain_view.call("set_contours", _contours_effective())
|
||
if contours_btn != null and contours_btn.button_pressed != _contours_on:
|
||
contours_btn.set_pressed_no_signal(_contours_on)
|
||
|
||
|
||
# --- Baum / Auswahl-Sync (U1: Module, dünne Weiterleitung) -------------------
|
||
|
||
func _refresh_tree() -> void:
|
||
tree_mod.refresh_tree()
|
||
|
||
|
||
## Baum-Klicks kommen im Modul an; die Viewport-Seite (interaction.gd) ruft
|
||
## weiter main.call("_mark_tree_selection…") — hier weiterleiten.
|
||
func _mark_tree_selection(idx: int) -> void:
|
||
tree_mod.mark_tree_selection(idx)
|
||
|
||
|
||
func _mark_tree_selection_multi(indices: Array) -> void:
|
||
tree_mod.mark_tree_selection_multi(indices)
|
||
|
||
|
||
## Rechte Panel-Sektion synchronisieren (wird von interaction.gd gerufen).
|
||
func _sync_selection_panel() -> void:
|
||
selection_mod.sync_selection()
|
||
|
||
|
||
## Hilfs-Meldung zur frischen Auswahl (wird von interaction.gd gerufen).
|
||
func _selection_status(idx: int) -> void:
|
||
selection_mod.selection_status(idx)
|
||
|
||
|
||
func _refresh_stats() -> void:
|
||
var count := SvmapDocument.object_count(doc)
|
||
var poly := SvmapDocument.poly_usage(doc, manifest)
|
||
var budget := int(SvmapIO.MAX_TOTAL_POLY)
|
||
stats_objects.text = "Objekte: %d / 5000 · Poly-Budget: %s / %s (%.0f %%) · Bauteile: %d" % [
|
||
count, _fmt_int(int(poly)), _fmt_int(budget), 100.0 * poly / budget,
|
||
BuildOps.piece_count(doc)]
|
||
# GLB-Fehlende Objekte (Karte t_9b50b231): Platzhalter laufen, aber Sly soll
|
||
# sehen, WELCHE echten Modelle im Mesh-Lager nachgelegt werden müssen.
|
||
if view_builder != null and not (view_builder.get("fehlende_glbs") as Array).is_empty():
|
||
var fehlend: Array = view_builder.get("fehlende_glbs")
|
||
var nennung := ", ".join(fehlend.slice(0, 3))
|
||
if fehlend.size() > 3:
|
||
nennung += " … (+%d weitere)" % (fehlend.size() - 3)
|
||
stats_objects.text += " · ⚠ GLB fehlt: %s" % nennung
|
||
# Terrain-Anteil am Quest-3-Budget (res 513 = 524.288 Dreiecke, map_format.md §4)
|
||
var tris := 0
|
||
var res := 0
|
||
if view_builder != null and view_builder.get("terrain_view") != null:
|
||
tris = int(view_builder.terrain_view.call("triangle_count"))
|
||
res = int(view_builder.terrain_view.call("resolution"))
|
||
var quest_budget := 524288
|
||
var pct := 100.0 * float(tris) / float(quest_budget)
|
||
var fps := Engine.get_frames_per_second()
|
||
stats_terrain.text = " · Terrain: %s² Grid, %s Dreiecke (%.0f %% Quest-Budget) · %d FPS" % [
|
||
str(res) if res > 0 else "—", _fmt_int(tris), pct, fps]
|
||
|
||
|
||
func _refresh_file_label() -> void:
|
||
var map_name := str(doc.get("info", {}).get("name", "Ohne Namen"))
|
||
if current_path.is_empty():
|
||
file_label.text = "„%s“ (noch nicht gespeichert)%s" % [map_name, " •" if dirty else ""]
|
||
else:
|
||
file_label.text = "„%s“ — %s%s" % [map_name, current_path.get_file(), " •" if dirty else ""]
|
||
# Karte t_297221b5: bei egalisiertem Label (große Stufe, schmales Fenster)
|
||
# steht der volle Name im Tooltip.
|
||
file_label.tooltip_text = file_label.text
|
||
# U7 (Karte t_2336c58e): ungespeicherte Änderungen auch im FENSTERTITEL —
|
||
# so sieht man sie selbst dann, wenn die Datei-Leiste egalisiert ist.
|
||
get_window().title = "%s — %s%s" % [APP_TITLE, map_name,
|
||
" (ungespeichert)" if dirty else ""]
|
||
|
||
|
||
func _set_status(text: String, is_error := false) -> void:
|
||
if status_label == null or not is_instance_valid(status_label):
|
||
return # UI wurde gerade neu aufgebaut (UI-Größe) — nichts anfassen
|
||
status_label.text = text
|
||
# U7: Statusfarben über das Theme (Editor/colors/status_error/_ok).
|
||
status_label.add_theme_color_override("font_color",
|
||
get_theme_color("status_error" if is_error else "status_ok", "Editor"))
|
||
|
||
|
||
func _fmt_int(value: int) -> String:
|
||
var s := str(value)
|
||
var out := ""
|
||
while s.length() > 3:
|
||
out = "." + s.substr(s.length() - 3) + out
|
||
s = s.substr(0, s.length() - 3)
|
||
return s + out
|
||
|
||
|
||
# ------------------------------------------------------- U1 Dock-Zustand ----
|
||
|
||
## Splitter-Lagen (Assets-Höhe) aus der Config lesen — NACH _build_ui.
|
||
func _load_dock_state() -> void:
|
||
var cfg := ConfigFile.new()
|
||
if cfg.load(UIScale.cfg_path()) == OK:
|
||
var l: Variant = cfg.get_value(UIScale.WIN_SECTION, "dock_links", null)
|
||
if l is float or l is int:
|
||
_dock_offsets["links"] = float(l)
|
||
var r: Variant = cfg.get_value(UIScale.WIN_SECTION, "dock_rechts", null)
|
||
if r is float or r is int:
|
||
_dock_offsets["rechts"] = float(r)
|
||
var u: Variant = cfg.get_value(UIScale.WIN_SECTION, "dock_unten", null)
|
||
if u is float or u is int:
|
||
_dock_offsets["unten"] = float(u)
|
||
_apply_dock_state()
|
||
|
||
|
||
## Gemerkte Lagen anwenden — deferred, weil das Layout erst nach dem ersten
|
||
## Layout-Durchlauf vermessene Größen hat.
|
||
func _apply_dock_state() -> void:
|
||
if DisplayServer.get_name() == "headless":
|
||
return
|
||
_apply_dock_state_deferred.call_deferred()
|
||
|
||
|
||
func _apply_dock_state_deferred() -> void:
|
||
if _dock_offsets.is_empty():
|
||
return
|
||
var main_split := _find_split("MainSplit")
|
||
if main_split != null and _dock_offsets.has("links"):
|
||
main_split.split_offset = UIScale.f(float(_dock_offsets["links"]))
|
||
var bottom := _find_split("BottomSplit")
|
||
if bottom != null and _dock_offsets.has("unten"):
|
||
bottom.split_offset = -UIScale.f(float(_dock_offsets["unten"]))
|
||
|
||
|
||
func _find_split(sname: String) -> SplitContainer:
|
||
var found := find_children(sname, "SplitContainer", true, false)
|
||
return found[0] if not found.is_empty() else null
|
||
|
||
|
||
## Splitter-Lagen merken (beim Schließen, neben der Fensterlage).
|
||
func _save_dock_state() -> void:
|
||
if DisplayServer.get_name() == "headless":
|
||
return
|
||
var main_split := _find_split("MainSplit")
|
||
var bottom := _find_split("BottomSplit")
|
||
var cfg := ConfigFile.new()
|
||
cfg.load(UIScale.cfg_path())
|
||
if main_split != null:
|
||
cfg.set_value(UIScale.WIN_SECTION, "dock_links", main_split.split_offset)
|
||
if bottom != null:
|
||
cfg.set_value(UIScale.WIN_SECTION, "dock_unten", float(-bottom.split_offset))
|
||
cfg.save(UIScale.cfg_path())
|
||
|
||
|
||
# ---------------------------------------------------- Screenshot-Demo -------
|
||
|
||
## Desktop-Smoke-Hilfe: `godot ... -- --ghost-demo [x z objekt-id]` parkt die
|
||
## Platzier-Vorschau sichtbar in der Map und zoomt die Kamera heran, damit ein
|
||
## Screenshot die Vorschau zeigen kann (nur für Smoke-Tests).
|
||
func _maybe_start_demo_mode() -> void:
|
||
var args := OS.get_cmdline_user_args()
|
||
if args.has("--terrain-demo"):
|
||
_start_terrain_demo(args)
|
||
return
|
||
if args.has("--library-demo"):
|
||
_start_library_demo(args)
|
||
return
|
||
if args.has("--spawns-demo"):
|
||
_start_spawns_demo(args)
|
||
return
|
||
if args.has("--groups-demo"):
|
||
_start_groups_demo(args)
|
||
return
|
||
if args.has("--bauen-demo"):
|
||
_start_bauen_demo(args)
|
||
return
|
||
if args.has("--ui-demo"):
|
||
_start_ui_demo(args)
|
||
return
|
||
if not args.has("--ghost-demo"):
|
||
return
|
||
var oid := "crate_wood"
|
||
var x := 0.0
|
||
var z := 0.0
|
||
for i in args.size():
|
||
if args[i] == "--ghost-demo":
|
||
if i + 1 < args.size():
|
||
x = float(args[i + 1])
|
||
if i + 2 < args.size():
|
||
z = float(args[i + 2])
|
||
if i + 3 < args.size():
|
||
oid = args[i + 3]
|
||
var pos := Vector3(x, SvmapDocument.terrain_height_at(doc, x, z), z)
|
||
interaction.demo_park_ghost(pos, oid)
|
||
# Demo-Aufbau: zwei Objekte setzen und eins orange auswählen — zeigt
|
||
# Ghost-Preview UND Auswahl-Markierung in einem Smoke-Screenshot.
|
||
if args.has("--ghost-demo-select"):
|
||
var before: Dictionary = history.snapshot(["objects"])
|
||
(doc["objects"] as Array).append({"id": "watchtower_wood", "pos": [6.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
(doc["objects"] as Array).append({"id": "crate_wood", "pos": [0.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
history.commit("Demo: Objekte gesetzt", before, history.snapshot(["objects"]))
|
||
interaction.call("clear_selection", true)
|
||
_refresh_all()
|
||
interaction.select_object(1, true)
|
||
else:
|
||
_refresh_history_buttons()
|
||
# Kamera nah heran, damit die Vorschau im Screenshot gut sichtbar ist.
|
||
camera.target = pos + Vector3(0.0, 0.6, 0.0)
|
||
camera.distance = 14.0
|
||
camera.pitch_deg = 32.0
|
||
camera.yaw_deg = 40.0
|
||
camera.call("_apply")
|
||
|
||
|
||
# ----------------------------------------------------- UI-Größen-Demo-Screens --
|
||
|
||
## `godot ... -- --ui-demo [ordner] [--ui-demo-res BxH] [--ui-demo-quit]`
|
||
## (Karte t_297221b5): fährt ALLE UI-Größen-Stufen programmatisch durch —
|
||
## pro Stufe kompletter UI-Neuaufbau (derselbe Pfad wie der Menü-Klick) und
|
||
## ein Fenster-Screenshot. Beweist: umschaltbar im Lauf + nichts abgeschnitten.
|
||
func _start_ui_demo(args: PackedStringArray) -> void:
|
||
var out_dir := "/tmp/scopedvr_ui"
|
||
var idx := args.find("--ui-demo")
|
||
if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"):
|
||
out_dir = args[idx + 1]
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
var res_idx := args.find("--ui-demo-res")
|
||
if res_idx >= 0 and res_idx + 1 < args.size():
|
||
var parts := str(args[res_idx + 1]).split("x")
|
||
if parts.size() == 2:
|
||
# Fenstergröße NICHT zu früh setzen — direkt bei _ready wird der
|
||
# Wert von der Projekt-Einstellung überschrieben (xvfb-Befund);
|
||
# erst nach dem ersten Frame ist der Resize stabil.
|
||
await get_tree().process_frame
|
||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||
DisplayServer.window_set_size(Vector2i(int(parts[0]), int(parts[1])))
|
||
get_window().size = Vector2i(int(parts[0]), int(parts[1])) # root-Window setzt durch (xvfb-Befund)
|
||
for i in UIScale.step_count():
|
||
UIScale.set_step_index(i)
|
||
_rebuild_ui_for_scale()
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
_set_status("UI-Größe: %s (%.2f-fach) — Theme-Schrift %d px, Beweis-Screenshot." % [
|
||
UIScale.step_label(i), UIScale.factor(), UIScale.font_size()])
|
||
await _demo_shot(out_dir + "/ui_%s.png" % UIScale.step_key(i))
|
||
print("UI_DEMO: Stufe %s Faktor %.2f Schrift %d" % [UIScale.step_key(i), UIScale.factor(), UIScale.font_size()])
|
||
print("UI_DEMO_OK: %d Stufen, PNGs in %s" % [UIScale.step_count(), out_dir])
|
||
if args.has("--ui-demo-quit"):
|
||
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden nach den Shots
|
||
|
||
|
||
# ------------------------------------------------ Terrain-Demo-Screenshots --
|
||
|
||
## `godot ... -- --terrain-demo [ordner]` — Desktop-Smoke (Karte t_9aba04f1):
|
||
## fährt die Terrain-Szenarien programmatisch durch und speichert PNGs:
|
||
## Hügel heben, Senke senken, glätten, Undo-Roundtrip. Die App bleibt offen
|
||
## (Sly sieht das Ergebnis live auf dem Desktop).
|
||
func _start_terrain_demo(args: PackedStringArray) -> void:
|
||
var out_dir := "/tmp/scopedvr_terrain"
|
||
var idx := args.find("--terrain-demo")
|
||
if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"):
|
||
out_dir = args[idx + 1]
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
|
||
_set_tool_mode("terrain")
|
||
terrain_mod.terrain_radius_slider.value = 45.0
|
||
terrain_mod.terrain_strength_slider.value = 10.0
|
||
terrain_mod.terrain_mode_raise.button_pressed = true
|
||
|
||
# 1) Hügel heben — Pinsel-Ring bleibt als visueller Beweis im Bild.
|
||
terrain_tool.call("demo_stroke", -80.0, -60.0, TerrainField.Mode.RAISE, 1.4)
|
||
terrain_tool.call("demo_park_brush", -80.0, -60.0)
|
||
_demo_camera(Vector3(-80.0, 12.0, -60.0), 150.0, 32.0, 40.0)
|
||
_set_status("Demo 1/5: Hügel GEHOBEN (Radius 45 m, Stärke 10 m/s) — der grüne Ring ist der Pinsel.")
|
||
await _demo_shot(out_dir + "/terrain_raise.png")
|
||
|
||
# 2) Senke senken (roter Ring).
|
||
terrain_mod.terrain_mode_lower.button_pressed = true
|
||
terrain_tool.call("demo_stroke", 90.0, 50.0, TerrainField.Mode.LOWER, 1.2)
|
||
terrain_tool.call("demo_park_brush", 90.0, 50.0)
|
||
_demo_camera(Vector3(90.0, 2.0, 50.0), 150.0, 32.0, -50.0)
|
||
_set_status("Demo 2/5: Senke GESSENKT (Strg+Linksklick senkt, ohne umzuschalten).")
|
||
await _demo_shot(out_dir + "/terrain_lower.png")
|
||
|
||
# 3) Hügelkuppe glätten (blauer Ring).
|
||
terrain_mod.terrain_mode_smooth.button_pressed = true
|
||
terrain_tool.call("demo_stroke", -70.0, -50.0, TerrainField.Mode.SMOOTH, 0.9)
|
||
terrain_tool.call("demo_park_brush", -70.0, -50.0)
|
||
_demo_camera(Vector3(-70.0, 10.0, -50.0), 130.0, 35.0, 20.0)
|
||
_set_status("Demo 3/5: Kuppe GLÄTTET — rau wird weich, ohne die Form zu zerstören.")
|
||
await _demo_shot(out_dir + "/terrain_smooth.png")
|
||
|
||
# 4) Undo-Roundtrip: kleiner Strich → Undo → Boden wieder flach.
|
||
terrain_mod.terrain_mode_raise.button_pressed = true
|
||
terrain_mod.terrain_radius_slider.value = 30.0
|
||
terrain_tool.call("demo_stroke", 0.0, 150.0, TerrainField.Mode.RAISE, 0.9)
|
||
terrain_tool.call("demo_park_brush", 0.0, 150.0)
|
||
_demo_camera(Vector3(0.0, 8.0, 150.0), 120.0, 35.0, 0.0)
|
||
_set_status("Demo 4/5: Test-Hügel für den Undo-Vergleich geformt.")
|
||
await _demo_shot(out_dir + "/terrain_before_undo.png")
|
||
_do_undo()
|
||
_set_status("Demo 5/5: Strg+Z — der GANZE Strich ist zurück, der Boden ist wieder flach.")
|
||
await _demo_shot(out_dir + "/terrain_after_undo.png")
|
||
|
||
print("TERRAIN_DEMO_OK: 5 Screenshots in %s" % out_dir)
|
||
|
||
|
||
func _demo_camera(target: Vector3, dist: float, pitch: float, yaw: float) -> void:
|
||
camera.target = target
|
||
camera.distance = dist
|
||
camera.pitch_deg = pitch
|
||
camera.yaw_deg = yaw
|
||
camera.call("_apply")
|
||
|
||
|
||
func _demo_shot(path: String) -> void:
|
||
for i in 5:
|
||
await get_tree().process_frame
|
||
await RenderingServer.frame_post_draw
|
||
var img := get_viewport().get_texture().get_image()
|
||
img.save_png(path)
|
||
print("DEMO_SHOT: %s (%d×%d)" % [path, img.get_width(), img.get_height()])
|
||
var tv = view_builder.get("terrain_view")
|
||
if tv != null and is_instance_valid(tv):
|
||
var mi = tv.get("_mi")
|
||
if mi != null and is_instance_valid(mi) and mi.mesh != null:
|
||
var aabb: AABB = mi.mesh.get_aabb()
|
||
print("DEMO_AABB: pos=%s size=%s tris=%d" % [aabb.position, aabb.size, tv.triangle_count()])
|
||
print("DEMO_CAM: cam=%s target=%s" % [camera.global_position, camera.target])
|
||
|
||
|
||
## `godot ... -- --library-demo [ordner]` — Desktop-Smoke (Karte t_3a998ae4):
|
||
## Gruppen-Bibliothek + platzierte Objekte + Snap-to-Ground programmatisch
|
||
## durchspielen, PNGs speichern. Die App bleibt offen.
|
||
func _start_library_demo(args: PackedStringArray) -> void:
|
||
var out_dir := "/tmp/scopedvr_library"
|
||
var idx := args.find("--library-demo")
|
||
if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"):
|
||
out_dir = args[idx + 1]
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
|
||
# 1) Bibliothek-Browser: Gruppen sichtbar, Ghost-Preview aktiv.
|
||
_set_status("Demo 1/3: Bibliothek in Gruppen — ein Objekt anklicken setzt eine Vorschau in die Karte.")
|
||
interaction.demo_park_ghost(Vector3(10.0, SvmapDocument.terrain_height_at(doc, 10.0, 6.0), 6.0), "watchtower_wood")
|
||
_demo_camera(Vector3(0.0, 2.0, 0.0), 90.0, 35.0, 40.0)
|
||
await _demo_shot(out_dir + "/library_browser.png")
|
||
|
||
# 2) Objekte platziert (versch. Gruppen), eins orange ausgewählt.
|
||
var before: Dictionary = history.snapshot(["objects"])
|
||
(doc["objects"] as Array).append({"id": "watchtower_wood", "pos": [10.0, 0.0, 6.0], "scale": 1.0, "rot_y_deg": 30.0})
|
||
(doc["objects"] as Array).append({"id": "crate_wood", "pos": [-8.0, 0.0, -4.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
(doc["objects"] as Array).append({"id": "tree_pine", "pos": [-20.0, 0.0, 18.0], "scale": 1.0, "rot_y_deg": 75.0})
|
||
(doc["objects"] as Array).append({"id": "rock_large", "pos": [24.0, 0.0, -18.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
history.commit("Demo: Objekte gesetzt", before, history.snapshot(["objects"]))
|
||
interaction.call("cancel_placement")
|
||
_clear_edit_state()
|
||
_refresh_all()
|
||
interaction.select_object(0, true)
|
||
_demo_camera(Vector3(0.0, 2.0, 0.0), 110.0, 40.0, -35.0)
|
||
_set_status("Demo 2/3: Objekte platziert (Wachturm orange ausgewählt) — ziehen, drehen, löschen wie gehabt.")
|
||
await _demo_shot(out_dir + "/library_placed.png")
|
||
|
||
# 3) Snap to Ground: Objekte künstlich in die Luft hängen, dann 1 Klick.
|
||
var float_before: Dictionary = history.snapshot(["objects"])
|
||
for obj in (doc["objects"] as Array):
|
||
var pos: Array = obj["pos"]
|
||
obj["pos"] = [float(pos[0]), float(pos[1]) + 7.0, float(pos[2])]
|
||
history.commit("Demo: Objekte schweben", float_before, history.snapshot(["objects"]))
|
||
_refresh_all()
|
||
_demo_camera(Vector3(0.0, 6.0, 0.0), 110.0, 30.0, -35.0)
|
||
_set_status("Demo 3/3a: Objekte SCHWEBEN nach dem Terrain-Formen — ein Klick auf „Alles auf den Boden setzen\" …")
|
||
await _demo_shot(out_dir + "/library_floating.png")
|
||
_on_snap_to_ground()
|
||
_demo_camera(Vector3(0.0, 4.0, 0.0), 110.0, 38.0, -35.0)
|
||
_set_status("Demo 3/3b: … und schon stehen ALLE wieder auf dem Boden (Strg+Z nimmt den ganzen Klick zurück).")
|
||
await _demo_shot(out_dir + "/library_after_snap.png")
|
||
print("LIBRARY_DEMO_OK: 4 Screenshots in %s" % out_dir)
|
||
if args.has("--library-demo-quit"):
|
||
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind
|
||
|
||
|
||
## `godot ... -- --spawns-demo [ordner]` — Desktop-Smoke (Karte t_c4162c5d):
|
||
## Spawn-Zonen-Workflow programmatisch durchspielen, PNGs speichern:
|
||
## Zonen-Übersicht → Live-Check schlägt Alarm bei fehlenden Zonen → Undo rettet →
|
||
## Map-Check-Dialog meldet bereit. Die App bleibt offen (Sly sieht das live).
|
||
func _start_spawns_demo(args: PackedStringArray) -> void:
|
||
var out_dir := "/tmp/scopedvr_spawns"
|
||
var idx := args.find("--spawns-demo")
|
||
if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"):
|
||
out_dir = args[idx + 1]
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
|
||
# 1) Übersicht: Default-Zonen beider Teams (Kreise) + rote Ghost-Vorschau.
|
||
_set_tool_mode("spawns")
|
||
check_mod.spawn_team_red.button_pressed = true
|
||
spawn_tool.call("demo_park_ghost", 260.0, 120.0, 1, 12.0)
|
||
_demo_camera(Vector3(160.0, 140.0, 0.0), 700.0, 48.0, -90.0)
|
||
_set_status("Demo 1/4: Spawn-Zonen — blaue Kreise = Team Blau, rote = Team Rot, halbtransparent folgt der Ghost der Maus als Vorschau.")
|
||
await _demo_shot(out_dir + "/spawns_zones.png")
|
||
|
||
# 2) ALLE roten Zonen löschen → Live-Check springt auf ❌ (BEVOR exportiert wird).
|
||
_clear_edit_state()
|
||
var before: Dictionary = SpawnOps.snapshot_blocks(history, doc)
|
||
for p in SpawnOps.points(doc, 1).duplicate():
|
||
SpawnOps.remove_point(doc, 1, str(p["id"]))
|
||
history.commit("Demo: alle roten Zonen gelöscht", before,
|
||
SpawnOps.snapshot_blocks(history, doc))
|
||
_after_structure_change()
|
||
_set_status("Demo 2/4: ALLE roten Zonen weg — der Spawn-Check (rechts) zeigt sofort ❌: Export wäre blockiert.")
|
||
await _demo_shot(out_dir + "/spawns_check.png")
|
||
|
||
# 3) Strg+Z — alle vier roten Zonen sind zurück (EIN Undo-Schritt).
|
||
_do_undo()
|
||
_set_status("Demo 3/4: Strg+Z — alle 4 roten Zonen sind auf EINMAL zurück.")
|
||
await _demo_shot(out_dir + "/spawns_undo.png")
|
||
|
||
# 4) Karte prüfen: Klartext-Liste im Dialog + Status „Alles bereit“.
|
||
_on_check_map()
|
||
_set_status("Demo 4/4: „Karte prüfen“ — alles ✅ … beim Speichern wird die .svmap automatisch signiert (sha256).")
|
||
await _demo_shot(out_dir + "/spawns_ready.png")
|
||
print("SPAWNS_DEMO_OK: 4 Screenshots in %s" % out_dir)
|
||
if args.has("--spawns-demo-quit"):
|
||
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind
|
||
|
||
|
||
## `godot ... -- --groups-demo [ordner]` — Desktop-Smoke (Karte t_f6f29d86):
|
||
## Auswahl-System programmatisch durchspielen, PNGs speichern:
|
||
## Mehrfachauswahl (Rahmen simuliert) → gruppieren → Gruppe gemeinsam ziehen →
|
||
## gemeinsames Drehen → Undo-Nachweis. Die App bleibt offen (Sly sieht live).
|
||
func _start_groups_demo(args: PackedStringArray) -> void:
|
||
var out_dir := "/tmp/scopedvr_groups"
|
||
var idx := args.find("--groups-demo")
|
||
if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"):
|
||
out_dir = args[idx + 1]
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
|
||
# Aufbau: 4 Objekte in zwei Wohnclustern (2 nahe beieinander = spätere Gruppe).
|
||
var before: Dictionary = history.snapshot(["objects"])
|
||
(doc["objects"] as Array).append({"id": "watchtower_wood", "pos": [10.0, 0.0, 6.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
(doc["objects"] as Array).append({"id": "crate_wood", "pos": [14.0, 0.0, 9.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
(doc["objects"] as Array).append({"id": "crate_metal", "pos": [14.0, 0.0, 13.0], "scale": 1.0, "rot_y_deg": 45.0})
|
||
(doc["objects"] as Array).append({"id": "tree_pine", "pos": [-30.0, 0.0, 24.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||
history.commit("Demo: Objekte gesetzt", before, history.snapshot(["objects"]))
|
||
_refresh_all()
|
||
|
||
# 1) Mehrfachauswahl: Objekte 0–2 markiert (wie nach Rahmen-Ausziehen).
|
||
interaction.call("select_many", [0, 1, 2], true)
|
||
_demo_camera(Vector3(0.0, 2.0, 0.0), 130.0, 40.0, -35.0)
|
||
_set_status("Demo 1/4: Mehrfachauswahl — 3 Objekte orange (Umschalt+Klick oder Rahmen aufziehen). Der Baum links bleibt außen vor.")
|
||
await _demo_shot(out_dir + "/groups_select.png")
|
||
|
||
# 2) Gruppieren: EIN Klick, die 3 gehören ab jetzt zusammen.
|
||
interaction.call("group_selection")
|
||
_set_status("Demo 2/4: „Gruppieren“ — die 3 Objekte sind jetzt Gruppe 1. Der Baum links ist NICHT Teil der Gruppe.")
|
||
await _demo_shot(out_dir + "/groups_grouped.png")
|
||
|
||
# 3) Gruppe gemeinsam ziehen (exakt der Drag-Release-Pfad) + gemeinsames Drehen.
|
||
interaction.call("demo_drag_selection", Vector3(24.0, 0.0, -18.0))
|
||
interaction.call("rotate_selected", 30.0)
|
||
_demo_camera(Vector3(20.0, 2.0, 0.0), 130.0, 42.0, -20.0)
|
||
_set_status("Demo 3/4: Gruppe GEMEINSAM verschoben und um die eigene Mitte gedreht — die Anordnung bleibt erhalten, der Baum stand still.")
|
||
await _demo_shot(out_dir + "/groups_moved.png")
|
||
|
||
# 4) Undo-Nachweis: Strg+Z nimmt die Drehung zurück (EIN Schritt), die
|
||
# zweite Stufe würde den Zug zurücknehmen — jede Aktion = EIN Undo-Schritt.
|
||
_do_undo()
|
||
_demo_camera(Vector3(20.0, 2.0, 0.0), 130.0, 42.0, -20.0)
|
||
_set_status("Demo 4/4: Strg+Z — die Drehung ist zurück (Zug & Drehen sind getrennte Schritte); die Gruppe bleibt gebunden.")
|
||
await _demo_shot(out_dir + "/groups_undo.png")
|
||
print("GROUPS_DEMO_OK: 4 Screenshots in %s" % out_dir)
|
||
if args.has("--groups-demo-quit"):
|
||
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind
|
||
|
||
|
||
## `godot ... -- --bauen-demo [ordner]` — Desktop-Smoke (Karte t_97ea59c5):
|
||
## baut programmatisch ein kleines Haus (Fundament+Wände+Giebel+Dach+Tür-Wand
|
||
## +Treppe), zeigt Auswahl + Ghost, speichert/lädt die Map zum Beweis. Die App
|
||
## bleibt offen (Sly sieht das Ergebnis live).
|
||
func _start_bauen_demo(args: PackedStringArray) -> void:
|
||
var out_dir := "/tmp/scopedvr_bauen"
|
||
var idx := args.find("--bauen-demo")
|
||
if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"):
|
||
out_dir = args[idx + 1]
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
|
||
_set_tool_mode("bauen")
|
||
var bt = build_tool # untypisiert (script-erweitert) — kein := möglich
|
||
# 1) Ghost-Vorschau am Cursor + erstes Fundament.
|
||
bt.call("demo_park_ghost", "wand", Vector3(2.0, 0.2, 2.0), 0.0)
|
||
_demo_camera(Vector3(3.0, 2.0, 8.0), 14.0, 32.0, 25.0)
|
||
_set_status("Demo 1/3: Bauen-Modus — Bauteil rechts anklicken, die gelbe Vorschau rastet aufs 2-m-Raster.")
|
||
await _demo_shot(out_dir + "/bauen_ghost.png")
|
||
# 2) Kleines Haus komplett gesetzt (jedes Teil = EIN Undo-Schritt):
|
||
# Fundament, 3 Wände, Tür-Wand, 2 Giebelwände, Dach, Treppe.
|
||
bt.call("demo_place", "fundament", Vector3(3.0, 0.0, 3.0), 0.0)
|
||
bt.call("demo_place", "wand", Vector3(2.0, 0.2, 2.0), 0.0)
|
||
bt.call("demo_place", "wand", Vector3(2.0, 0.2, 4.0), 0.0)
|
||
bt.call("demo_place", "wand", Vector3(2.0, 0.2, 4.0), 90.0)
|
||
bt.call("demo_place", "tuer", Vector3(4.0, 0.2, 4.0), 90.0)
|
||
bt.call("demo_place", "giebelwand", Vector3(2.0, 2.2, 2.0), 0.0)
|
||
bt.call("demo_place", "giebelwand", Vector3(2.0, 2.2, 4.0), 0.0)
|
||
bt.call("demo_place", "dach_giebel", Vector3(3.0, 2.2, 3.0), 0.0)
|
||
bt.call("demo_place", "treppe", Vector3(8.0, 0.0, 7.0), 0.0)
|
||
_after_structure_change()
|
||
bt.call("select_index", 7) # Dach ausgewählt (orangener Rahmen)
|
||
_demo_camera(Vector3(9.0, 4.5, 10.0), 16.0, 35.0, 40.0)
|
||
_set_status("Demo 2/3: Kleines Haus gebaut — Fundament, Wände, Tür-Öffnung, Giebelwände, Dach, Treppe. Dach ist ausgewählt (orange).")
|
||
await _demo_shot(out_dir + "/bauen_haus.png")
|
||
# 3) Speichern → Laden → weiterbauen (Abnahme-Flow): Map roundtrippen,
|
||
# dann eine zusätzliche Wand — sie muss nach dem Laden sauber passen.
|
||
_save_to(out_dir + "/bauen_demo.svmap")
|
||
var reload := SvmapIO.load_from_file(out_dir + "/bauen_demo.svmap")
|
||
if bool(reload["ok"]):
|
||
doc = reload["data"]
|
||
history.begin(doc)
|
||
_clear_edit_state()
|
||
_refresh_all()
|
||
var before: Dictionary = history.snapshot(["gebaeude"])
|
||
BuildOps.ensure_block(doc).append(BuildOps.make_piece("fenster", Vector3(2.0, 0.2, 4.0), 90.0))
|
||
history.commit("Wand mit Fenster gesetzt", before, history.snapshot(["gebaeude"]))
|
||
_after_structure_change()
|
||
bt.call("demo_park_ghost", "fenster", Vector3(6.0, 0.2, 4.0), 90.0)
|
||
_demo_camera(Vector3(9.0, 4.5, 10.0), 16.0, 35.0, 40.0)
|
||
_set_status("Demo 3/3: Gespeichert, geladen und WEITERGEBAUT (Fenster-Wand dazwischen) — Strg+Z nimmt nur das neue Teil zurück.")
|
||
await _demo_shot(out_dir + "/bauen_weiter.png")
|
||
print("BAUEN_DEMO_OK: 3 Screenshots in %s (%d Bauteile, Map OK)" % [
|
||
out_dir, BuildOps.piece_count(doc)])
|
||
if args.has("--bauen-demo-quit"):
|
||
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind
|
||
|
||
|
||
## --- Test-Start: Haken (T1b-Vertrag, Operator-Wiederanbindung 20.10.) --------
|
||
## Diese drei Namen erwartet tests/test_playtest_spawn.gd (56 checks) auf main.
|
||
## Sie waren beim U1-Umbau verschwunden (Suite brach mit „Nonexistent function
|
||
## '_test_start_reload'" ab und hing) — deshalb stehen sie hier bewusst fest.
|
||
## Der sichtbare Teil (Info + Löschen) lebt in src/ui/panels/test_start_panel.gd,
|
||
## der Zustand hier — genau wie vor dem Umbau.
|
||
func _test_start_apply(x: float, z: float, hinweis: String) -> void:
|
||
test_start_point = Vector3(x, 0.0, z)
|
||
TestStartStore.save_point(current_path, test_start_point)
|
||
if test_start_tool != null:
|
||
test_start_tool.call("refresh_from_doc")
|
||
_refresh_test_start_panel()
|
||
# Klartext-Hinweis des Werkzeugs (z.B. „auf dem Boden unter dem Objekt")
|
||
# mitnehmen — die Prüfsuite liest ihn aus der Statuszeile.
|
||
var ort := " (Höhe automatisch)" if hinweis.is_empty() else " (Höhe automatisch — %s)" % hinweis
|
||
_set_status("Test-Start: x=%.0f z=%.0f%s — F5 startet den Testlauf genau dort." % [x, z, ort])
|
||
|
||
|
||
func _test_start_delete() -> void:
|
||
if test_start_point == null:
|
||
return
|
||
test_start_point = null
|
||
TestStartStore.clear_point(current_path)
|
||
if test_start_tool != null:
|
||
test_start_tool.call("refresh_from_doc")
|
||
_refresh_test_start_panel()
|
||
|
||
|
||
func _test_start_reload() -> void:
|
||
test_start_point = TestStartStore.load_point(current_path)
|
||
if test_start_tool != null:
|
||
test_start_tool.call("refresh_from_doc")
|
||
_refresh_test_start_panel()
|
||
|
||
|
||
func _refresh_test_start_panel() -> void:
|
||
if test_start_panel != null and is_instance_valid(test_start_panel):
|
||
test_start_panel.call("refresh_point", test_start_point)
|
||
|
||
|
||
## --- Test-Start: Werkzeug-Modus (wie Terrain/Spawns/Bauen) --------------------
|
||
## Die Prüfsuite erwartet tool_mode == "teststart", einen leuchtenden Leisten-
|
||
## Knopf (`test_start_btn`) und das erst dann sichtbare Panel (`test_start_panel`).
|
||
func _enter_test_start_mode() -> void:
|
||
interaction.call("cancel_placement")
|
||
tool_mode = "teststart"
|
||
if test_start_tool != null:
|
||
test_start_tool.call("activate")
|
||
_sync_test_start_button()
|
||
_sync_test_start_panel_visibility()
|
||
|
||
|
||
func _leave_test_start_mode() -> void:
|
||
if test_start_tool != null:
|
||
test_start_tool.call("deactivate")
|
||
# Direkt aus-/einschalten statt über tool_mode: _set_tool_mode ruft die
|
||
# _leave_*-Funktionen BEVOR der Zielmodus gesetzt ist — ein Abgleich gegen
|
||
# tool_mode ließe das Panel offen (Befund aus test_playtest_spawn).
|
||
if test_start_panel != null:
|
||
test_start_panel.visible = false
|
||
if test_start_btn != null:
|
||
test_start_btn.button_pressed = false
|
||
|
||
|
||
func _sync_test_start_button() -> void:
|
||
if test_start_btn != null:
|
||
test_start_btn.button_pressed = (tool_mode == "teststart")
|
||
|
||
|
||
func _sync_test_start_panel_visibility() -> void:
|
||
if test_start_panel != null:
|
||
test_start_panel.visible = (tool_mode == "teststart")
|
||
|
||
|
||
## Leisten-Knopf „🎯 Test-Start" (registry.gd ACTIONS) — schaltet das Werkzeug um.
|
||
func _on_test_start_tool_pressed() -> void:
|
||
_set_tool_mode("objects" if tool_mode == "teststart" else "teststart") |