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")