extends Control ## ScopedVR Map-Editor — Hauptfenster (M2 UX-Fundament, Karte t_40d855d0). ## ## Alles UI wird hier per Code gebaut (eine einzige .tscn mit Root-Knoten). ## Struktur: Top-Bar (Menü + Rückgängig/Wiederholen) | links Objekt-Bibliothek ## | Mitte 3D-Viewport | rechts Map-Eigenschaften + Auswahl | unten Statusleiste. ## ## UX-Fundament dieser Karte (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 (orange), 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") 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 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) # UI-Referenzen var content_tree: Tree var stats_objects: Label var stats_terrain: Label var status_label: Label var file_label: Label var help_label: Label var view_builder: Node3D var camera: Camera3D var library_box: VBoxContainer var library_buttons: Dictionary = {} # id → Button var undo_btn: Button var redo_btn: Button var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014) var _grid_on := false # Nutzer-Wahl für das Maßstabsgitter # Auswahl-Panel var sel_label: Label var sel_rotate_btn: Button var sel_delete_btn: Button var sel_dup_btn: Button # Objekt-Ops (Karte t_7567a06f) var sel_mirror_x_btn: Button var sel_mirror_z_btn: Button var sel_raise_btn: Button var sel_lower_btn: Button var sel_group_btn: Button # „Gruppieren“ (Karte t_f6f29d86) var sel_ungroup_btn: Button # „Gruppe lösen“ (Karte t_f6f29d86) # Rahmen-Ausziehen (Rubberband) — Overlay über dem 3D-Viewport var rubber_overlay: Control var rubber_rect := Rect2() # leer = Overlay aus # Eigenschaften-Panel var prop_name: LineEdit var prop_author: LineEdit var prop_desc: LineEdit var prop_version: SpinBox var prop_weather: OptionButton var terrain_info: Label # Terrain-Werkzeug-Panel (Karte t_9aba04f1) var terrain_edit_btn: Button var terrain_panel: VBoxContainer var terrain_mode_raise: Button var terrain_mode_lower: Button var terrain_mode_smooth: Button var terrain_mode_paint: Button var terrain_palette: VBoxContainer var terrain_mat_buttons: Array = [] # Gras/Sand/Fels (ButtonGroup) var terrain_size_btn_small: Button var terrain_size_btn_medium: Button var terrain_size_btn_large: Button var terrain_radius_slider: HSlider var terrain_radius_label: Label var terrain_strength_slider: HSlider var terrain_strength_label: Label # Spawn-Zonen-Werkzeug (Karte t_c4162c5d) — großer deutscher Button + Panel var spawn_edit_btn: Button var spawn_panel: VBoxContainer var spawn_team_blue: Button var spawn_team_red: Button var spawn_radius_slider: HSlider var spawn_radius_label: Label var spawn_status: Label # Live-Validierung (IMMER sichtbar, Loader-Regeln) var spawn_sel_label: Label # Welche Zone ist an? var spawn_delete_btn: Button # Bauen-Werkzeug (Karte t_97ea59c5) — großer deutscher Button + Bauteil-Panel var build_edit_btn: Button var build_panel: VBoxContainer var build_kind_buttons: Dictionary = {} # kind_id → Button # Map-Check (Karte prüfen) var check_btn: Button var check_dialog: AcceptDialog var check_text: Label # Karte testen (Karte t_b84af40f) var test_btn: Button # Dialoge var new_dialog: AcceptDialog var open_dialog: FileDialog var save_dialog: FileDialog var error_dialog: AcceptDialog var error_text: Label var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum func _ready() -> void: # InputMap-Actions der Tastatur-Shortcuts (Karte t_c46d31c2) — VOR UI/Kamera, # damit die Kamera-Polls (Flug W/A/S/D/E/Q) die Actions sofort kennen. ShortcutKeys.register() manifest = _load_manifest() _build_ui() _start_with_new_map() _maybe_start_demo_mode() 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 -- func _build_ui() -> void: var theme := Theme.new() theme.default_font_size = 16 self.theme = theme var vbox := VBoxContainer.new() vbox.set_anchors_preset(Control.PRESET_FULL_RECT) vbox.add_theme_constant_override("separation", 4) add_child(vbox) # --- Top-Bar: Menü + Rückgängig/Wiederholen --- var top := PanelContainer.new() var top_h := HBoxContainer.new() top_h.add_theme_constant_override("separation", 10) top.add_child(top_h) vbox.add_child(top) var file_menu := MenuButton.new() file_menu.text = "Datei" file_menu.flat = false file_menu.custom_minimum_size = Vector2(90, 40) var popup := file_menu.get_popup() popup.add_item("Neue Map…", 0) popup.add_item("Öffnen…", 1) popup.add_separator() popup.add_item("Speichern (Strg+S)", 2) popup.add_item("Speichern unter…", 3) popup.add_separator() popup.add_item("▶ Karte testen", 5) popup.add_separator() popup.add_item("Beenden", 4) popup.id_pressed.connect(_on_menu_action) top_h.add_child(file_menu) undo_btn = Button.new() undo_btn.text = "↩ Rückgängig" undo_btn.custom_minimum_size = Vector2(140, 40) undo_btn.tooltip_text = "Letzte Aktion zurücknehmen (Strg+Z)" undo_btn.pressed.connect(_do_undo) top_h.add_child(undo_btn) redo_btn = Button.new() redo_btn.text = "↪ Wiederholen" redo_btn.custom_minimum_size = Vector2(150, 40) redo_btn.tooltip_text = "Zurückgenommene Aktion wiederherstellen (Strg+Y)" redo_btn.pressed.connect(_do_redo) top_h.add_child(redo_btn) # Maßstabsgitter-Toggle (Karte t_7ac70014): über sichtbarem Terrain ist das # Gitter standardmäßig AUS (das Terrain ist die Bodendarstellung); wer das # 100-m-Raster als Planungshilfe will, schaltet es hier ohne Rebuild zu. grid_btn = Button.new() grid_btn.text = "⊞ Gitter" grid_btn.toggle_mode = true grid_btn.button_pressed = false grid_btn.custom_minimum_size = Vector2(120, 40) grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus." grid_btn.toggled.connect(_on_grid_toggled) top_h.add_child(grid_btn) # Map-Fertig-Check (Karte t_c4162c5d): Klartext-Liste VOR dem Export. check_btn = Button.new() check_btn.text = "✅ Karte prüfen" check_btn.custom_minimum_size = Vector2(170, 40) check_btn.tooltip_text = "Prüft, ob die Map bereit für den Export ist — sagen wir VORHER, was fehlt" check_btn.pressed.connect(_on_check_map) top_h.add_child(check_btn) # Karte testen (Karte t_b84af40f): Map SOFORT als Spiel zum Durchlaufen. test_btn = Button.new() test_btn.text = "▶ Karte testen" test_btn.custom_minimum_size = Vector2(190, 40) test_btn.tooltip_text = "Startet die Map SOFORT als Spiel zum Durchlaufen — eigenes Fenster, der Editor bleibt offen. ESC im Spiel = zurück zum Editor." test_btn.pressed.connect(_on_test_run) top_h.add_child(test_btn) var title_label := Label.new() title_label.text = " %s" % APP_TITLE title_label.add_theme_font_size_override("font_size", 20) top_h.add_child(title_label) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL top_h.add_child(spacer) file_label = Label.new() file_label.text = "" file_label.modulate = Color(1, 1, 1, 0.6) top_h.add_child(file_label) # --- Hauptbereich --- var split := HSplitContainer.new() split.size_flags_vertical = Control.SIZE_EXPAND_FILL split.size_flags_horizontal = Control.SIZE_EXPAND_FILL split.split_offset = -320 vbox.add_child(split) split.add_child(_build_left_panel()) split.add_child(_build_center_viewport()) split.add_child(_build_right_panel()) # --- Statusleiste: Hilfs-Meldungen + Budget --- var status_panel := PanelContainer.new() var status_h := HBoxContainer.new() status_h.add_theme_constant_override("separation", 16) status_panel.add_child(status_h) vbox.add_child(status_panel) status_label = Label.new() status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL status_label.clip_text = true # langer Text darf das Layout NICHT sprengen status_h.add_child(status_label) stats_objects = Label.new() stats_objects.modulate = Color(1, 1, 1, 0.75) status_h.add_child(stats_objects) # Terrain-Anteil + Editor-FPS (Perf-Zähler, Karte t_9aba04f1) stats_terrain = Label.new() stats_terrain.modulate = Color(1, 1, 1, 0.75) status_h.add_child(stats_terrain) var fps_timer := Timer.new() fps_timer.wait_time = 0.5 fps_timer.autostart = true fps_timer.timeout.connect(_refresh_stats) add_child(fps_timer) # --- Dialoge --- new_dialog = preload("res://src/ui/new_map_dialog.gd").new() new_dialog.confirmed.connect(_on_new_map_confirmed) add_child(new_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 = Vector2i(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 = Vector2i(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 = Vector2(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 = Vector2(620, 0) check_dialog.add_child(check_text) add_child(check_dialog) func _build_left_panel() -> PanelContainer: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(280, 0) var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", 6) panel.add_child(vbox) vbox.add_child(_header_label("Objekt-Bibliothek")) vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis Rechtsklick/ESC.")) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.custom_minimum_size = Vector2(0, 280) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED vbox.add_child(scroll) library_box = VBoxContainer.new() library_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL library_box.add_theme_constant_override("separation", 4) scroll.add_child(library_box) _fill_library() vbox.add_child(_separator()) vbox.add_child(_header_label("Objekt-Werkzeuge")) var snap_btn := Button.new() snap_btn.text = "⚡ Alles auf den Boden setzen" snap_btn.custom_minimum_size = Vector2(0, 44) snap_btn.tooltip_text = "Setzt ALLE Objekte auf die Terrain-Höhe — z. B. nach dem Terrain-Formen, wenn etwas in der Luft hängt" snap_btn.pressed.connect(_on_snap_to_ground) vbox.add_child(snap_btn) var grid_check := CheckButton.new() grid_check.text = "Raster-Snap (1 m)" grid_check.custom_minimum_size = Vector2(0, 40) grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein" grid_check.toggled.connect(_on_grid_snap_changed) vbox.add_child(grid_check) vbox.add_child(_muted_label("Raster-Snap rastet Platzieren und Ziehen auf ganze Meter.")) vbox.add_child(_separator()) vbox.add_child(_header_label("Map-Inhalt")) content_tree = Tree.new() content_tree.size_flags_vertical = Control.SIZE_EXPAND_FILL content_tree.custom_minimum_size = Vector2(0, 160) content_tree.allow_reselect = true content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2) content_tree.hide_root = true content_tree.item_selected.connect(_on_tree_selected) vbox.add_child(content_tree) return panel func _fill_library() -> void: library_buttons.clear() # Gruppen-Browser (Karte t_3a998ae4): Klapp-Überschriften je Gruppe, # darunter die Objekte mit Thumbnail (128-px-Render, runterskaliert). for group in LibraryBrowser.groups_in_order(manifest): var g_label: String = group["label"] var g_ids: Array = group["ids"] if g_ids.is_empty(): continue var header := Button.new() header.text = "▾ %s (%d)" % [g_label, g_ids.size()] header.toggle_mode = true header.button_pressed = true header.alignment = HORIZONTAL_ALIGNMENT_LEFT header.custom_minimum_size = Vector2(240, 40) library_box.add_child(header) var group_box := VBoxContainer.new() group_box.add_theme_constant_override("separation", 4) library_box.add_child(group_box) header.toggled.connect(func(on: bool) -> void: group_box.visible = on header.text = "%s %s (%d)" % ["▾" if on else "▸", g_label, g_ids.size()]) for oid in g_ids: _add_library_button(str(oid), group_box) func _add_library_button(oid: String, parent: Container) -> void: var lib: Dictionary = manifest.get("objects", {}) var entry: Dictionary = lib.get(oid, {}) var poly := int(entry.get("poly", 0)) var btn := Button.new() btn.text = "%s · %d Polys" % [LibraryBrowser.object_label(oid), poly] btn.icon = LibraryBrowser.load_thumb(oid) btn.custom_minimum_size = Vector2(240, 44) btn.alignment = HORIZONTAL_ALIGNMENT_LEFT btn.toggle_mode = true btn.button_pressed = false btn.pressed.connect(_on_library_pressed.bind(oid)) parent.add_child(btn) library_buttons[oid] = btn 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) 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) camera.set("wheel_filter", Callable(interaction, "wheel_blocked")) # Freelook-Flug-Verdrahtung (Karte t_c46d31c2): RMT gehört beim Platzieren # dem Ghost-Abbruch, Status-Meldungen der Kamera laufen ins Statusfeld. camera.set("rmb_filter", Callable(self, "_fly_rmb_blocked")) 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) # 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 = 12 help_label.offset_bottom = -8 help_label.offset_top = -46 help_label.modulate = Color(1, 1, 1, 0.65) 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 rubber_overlay.draw_rect(rubber_rect, Color(0.45, 0.8, 1.0, 0.16), true) rubber_overlay.draw_rect(rubber_rect, Color(0.45, 0.8, 1.0, 0.9), false, 2.0) func _build_right_panel() -> PanelContainer: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(320, 0) var scroll := ScrollContainer.new() scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED panel.add_child(scroll) var vbox := VBoxContainer.new() vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL vbox.add_theme_constant_override("separation", 8) scroll.add_child(vbox) # --- Auswahl (Anfassen/Greifen) --- vbox.add_child(_header_label("Auswahl")) sel_label = Label.new() sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART vbox.add_child(sel_label) var sel_btns := HBoxContainer.new() sel_btns.add_theme_constant_override("separation", 8) vbox.add_child(sel_btns) sel_rotate_btn = Button.new() sel_rotate_btn.text = "Drehen (R)" sel_rotate_btn.custom_minimum_size = Vector2(140, 40) sel_rotate_btn.tooltip_text = "Ausgewähltes Objekt 15° drehen (auch: Mausrad)" sel_rotate_btn.pressed.connect(func() -> void: interaction.rotate_selected(15.0)) sel_btns.add_child(sel_rotate_btn) sel_delete_btn = Button.new() sel_delete_btn.text = "Löschen (Entf)" sel_delete_btn.custom_minimum_size = Vector2(140, 40) sel_delete_btn.tooltip_text = "Ausgewähltes Objekt löschen (Strg+Z holt es zurück)" sel_delete_btn.pressed.connect(interaction.delete_selected) sel_btns.add_child(sel_delete_btn) # Objekt-Ops (Karte t_7567a06f): Duplizieren · Spiegeln · Anheben/Absenken. var dup_row := HBoxContainer.new() dup_row.add_theme_constant_override("separation", 8) vbox.add_child(dup_row) sel_dup_btn = _sel_op_button("⧉ Duplizieren (Strg+D)", "Legt eine Kopie daneben — leicht versetzt, direkt angewählt. Strg+D geht auch.", func() -> void: interaction.duplicate_selected()) dup_row.add_child(sel_dup_btn) var mirror_row := HBoxContainer.new() mirror_row.add_theme_constant_override("separation", 8) vbox.add_child(mirror_row) sel_mirror_x_btn = _sel_op_button("⇄ Links↔Rechts", "Spiegelt das Objekt an seiner Stelle (links↔rechts, X-Achse) — die Blickrichtung wird mitgespiegelt.", func() -> void: interaction.mirror_selected("x")) mirror_row.add_child(sel_mirror_x_btn) sel_mirror_z_btn = _sel_op_button("⇅ Vor↔Zurück", "Spiegelt das Objekt an seiner Stelle (vor↔zurück, Z-Achse) — die Blickrichtung wird mitgespiegelt.", func() -> void: interaction.mirror_selected("z")) mirror_row.add_child(sel_mirror_z_btn) var lift_row := HBoxContainer.new() lift_row.add_theme_constant_override("separation", 8) vbox.add_child(lift_row) sel_raise_btn = _sel_op_button("▲ Anheben (Bild↑)", "Objekt 0,25 m anheben — es schwebt dann, auch wenn der Boden darunter anders ist. Bild↑ geht auch.", func() -> void: interaction.lift_selected(ObjectOps.LIFT_STEP_M)) lift_row.add_child(sel_raise_btn) sel_lower_btn = _sel_op_button("▼ Absenken (Bild↓)", "Objekt 0,25 m absenken — stoppt am Boden (nichts wird in Hügel begraben). Bild↓ geht auch.", func() -> void: interaction.lift_selected(-ObjectOps.LIFT_STEP_M)) lift_row.add_child(sel_lower_btn) # Gruppen-Buttons (Karte t_f6f29d86): Mehrfachauswahl → Gruppe var group_btns := HBoxContainer.new() group_btns.add_theme_constant_override("separation", 8) vbox.add_child(group_btns) sel_group_btn = Button.new() sel_group_btn.text = "🔗 Gruppieren" sel_group_btn.custom_minimum_size = Vector2(150, 40) sel_group_btn.tooltip_text = "Ausgewählte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam" sel_group_btn.pressed.connect(func() -> void: interaction.group_selection()) group_btns.add_child(sel_group_btn) sel_ungroup_btn = Button.new() sel_ungroup_btn.text = "⛓ Gruppe lösen" sel_ungroup_btn.custom_minimum_size = Vector2(150, 40) sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich" sel_ungroup_btn.pressed.connect(func() -> void: interaction.ungroup_selection()) group_btns.add_child(sel_ungroup_btn) vbox.add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden.")) vbox.add_child(_separator()) # --- Map-Eigenschaften --- vbox.add_child(_header_label("Map-Eigenschaften")) vbox.add_child(_field_label("Name")) prop_name = LineEdit.new() prop_name.max_length = 64 vbox.add_child(prop_name) vbox.add_child(_field_label("Autor")) prop_author = LineEdit.new() vbox.add_child(prop_author) vbox.add_child(_field_label("Beschreibung")) prop_desc = LineEdit.new() vbox.add_child(prop_desc) vbox.add_child(_field_label("Version")) prop_version = SpinBox.new() prop_version.min_value = 1 prop_version.max_value = 99999 prop_version.step = 1 vbox.add_child(prop_version) vbox.add_child(_field_label("Wetter")) prop_weather = OptionButton.new() prop_weather.add_item("Mittag (sonnig, klar)") prop_weather.add_item("Abend (warmes Licht)") prop_weather.add_item("Regen (grau, kühl)") prop_weather.add_item("Nacht (dunkel)") vbox.add_child(prop_weather) vbox.add_child(_separator()) vbox.add_child(_header_label("Terrain")) terrain_info = Label.new() terrain_info.text = "" terrain_info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART vbox.add_child(terrain_info) # --- Terrain-Werkzeug (Karte t_9aba04f1): großer deutscher Button --- terrain_edit_btn = Button.new() terrain_edit_btn.text = "⛰ Terrain formen" terrain_edit_btn.custom_minimum_size = Vector2(0, 48) terrain_edit_btn.toggle_mode = true terrain_edit_btn.tooltip_text = "Boden heben, senken und glätten — direkt in der Karte malen" terrain_edit_btn.pressed.connect(_on_terrain_btn_pressed) vbox.add_child(terrain_edit_btn) terrain_panel = VBoxContainer.new() terrain_panel.add_theme_constant_override("separation", 6) terrain_panel.visible = false vbox.add_child(terrain_panel) terrain_panel.add_child(_field_label("Pinsel-Werkzeug:")) var mode_row := HBoxContainer.new() mode_row.add_theme_constant_override("separation", 6) terrain_panel.add_child(mode_row) var mode_group := ButtonGroup.new() terrain_mode_raise = _terrain_mode_button("Heben ⬆", mode_group) terrain_mode_lower = _terrain_mode_button("Senken ⬇", mode_group) terrain_mode_smooth = _terrain_mode_button("Glätten ≈", mode_group) terrain_mode_paint = _terrain_mode_button("Malen 🖌", mode_group) terrain_mode_raise.button_pressed = true terrain_mode_raise.toggled.connect(_on_terrain_mode_changed) terrain_mode_lower.toggled.connect(_on_terrain_mode_changed) terrain_mode_smooth.toggled.connect(_on_terrain_mode_changed) terrain_mode_paint.toggled.connect(_on_terrain_mode_changed) mode_row.add_child(terrain_mode_raise) mode_row.add_child(terrain_mode_lower) mode_row.add_child(terrain_mode_smooth) mode_row.add_child(terrain_mode_paint) # Material-Palette (Karte t_603ec1a8): nur sichtbar im Malen-Modus. terrain_palette = VBoxContainer.new() terrain_palette.add_theme_constant_override("separation", 4) terrain_palette.visible = false terrain_panel.add_child(terrain_palette) terrain_palette.add_child(_field_label("Material:")) var mat_row := HBoxContainer.new() mat_row.add_theme_constant_override("separation", 6) terrain_palette.add_child(mat_row) var mat_group := ButtonGroup.new() var mat_labels := ["🌿 Gras", "🏖 Sand", "🪨 Fels"] for mat_id in SvmapIO.MATERIAL_IDS.size(): var mat_btn := Button.new() mat_btn.text = str(mat_labels[mat_id]) mat_btn.custom_minimum_size = Vector2(0, 40) mat_btn.toggle_mode = true mat_btn.button_group = mat_group mat_btn.tooltip_text = "Malen: %s" % str(SvmapIO.MATERIAL_IDS[mat_id]) if mat_id == 0: mat_btn.button_pressed = true mat_btn.toggled.connect(_on_terrain_material_changed.bind(mat_id)) terrain_mat_buttons.append(mat_btn) mat_row.add_child(mat_btn) # Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider. terrain_panel.add_child(_field_label("Pinselgröße:")) var size_row := HBoxContainer.new() size_row.add_theme_constant_override("separation", 6) terrain_panel.add_child(size_row) var size_group := ButtonGroup.new() terrain_size_btn_small = _terrain_size_button("Klein\n(4 m)", size_group, 4.0) terrain_size_btn_medium = _terrain_size_button("Mittel\n(12 m)", size_group, 12.0) terrain_size_btn_large = _terrain_size_button("Groß\n(32 m)", size_group, 32.0) # Kein Signal: Der Handler greift auf terrain_radius_slider zu, der erst # unten erzeugt wird — beim Panel-Bau wäre das ein Crash (GUI-Start-Bug # 15.10.). Der Slider startet unten passend mit 12 m (= Mittel). terrain_size_btn_medium.set_pressed_no_signal(true) size_row.add_child(terrain_size_btn_small) size_row.add_child(terrain_size_btn_medium) size_row.add_child(terrain_size_btn_large) terrain_radius_label = _field_label("Pinsel-Größe: 12 m") terrain_panel.add_child(terrain_radius_label) terrain_radius_slider = HSlider.new() terrain_radius_slider.min_value = 2.0 terrain_radius_slider.max_value = 64.0 terrain_radius_slider.step = 1.0 terrain_radius_slider.value = 12.0 terrain_radius_slider.custom_minimum_size = Vector2(0, 32) terrain_radius_slider.value_changed.connect(_on_terrain_radius_changed) terrain_panel.add_child(terrain_radius_slider) terrain_strength_label = _field_label("Stärke: 8 m pro Sekunde") terrain_panel.add_child(terrain_strength_label) terrain_strength_slider = HSlider.new() terrain_strength_slider.min_value = 1.0 terrain_strength_slider.max_value = 20.0 terrain_strength_slider.step = 0.5 terrain_strength_slider.value = 8.0 terrain_strength_slider.custom_minimum_size = Vector2(0, 32) terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed) terrain_panel.add_child(terrain_strength_slider) terrain_panel.add_child(_muted_label( "Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Rechtsklick-Ziehen macht das Gegenteil — im Malen-Modus malt Rechtsklick Gras zurück. " + "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Terrain-Modus.")) # --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar --- vbox.add_child(_separator()) vbox.add_child(_header_label("Spawn-Check")) spawn_status = Label.new() spawn_status.text = "" spawn_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART spawn_status.custom_minimum_size = Vector2(0, 44) vbox.add_child(spawn_status) vbox.add_child(_muted_label("Der Check sagt VOR dem Export, was der Server-Loader verlangen wird (mindestens 4 Zonen pro Team, bekannte Objekt-IDs).")) # --- Spawn-Zonen-Werkzeug: großer Button + Bedien-Panel --- spawn_edit_btn = Button.new() spawn_edit_btn.text = "🚩 Spawn-Zonen malen" spawn_edit_btn.custom_minimum_size = Vector2(0, 48) spawn_edit_btn.toggle_mode = true spawn_edit_btn.tooltip_text = "Startzonen für Team Blau und Rot auf den Boden malen (TDM)" spawn_edit_btn.pressed.connect(_on_spawn_btn_pressed) vbox.add_child(spawn_edit_btn) spawn_panel = VBoxContainer.new() spawn_panel.add_theme_constant_override("separation", 6) spawn_panel.visible = false vbox.add_child(spawn_panel) spawn_panel.add_child(_field_label("Team:")) var team_row := HBoxContainer.new() team_row.add_theme_constant_override("separation", 6) spawn_panel.add_child(team_row) var team_group := ButtonGroup.new() spawn_team_blue = _spawn_team_button("🔵 Team Blau", team_group, 0) spawn_team_red = _spawn_team_button("🔴 Team Rot", team_group, 1) spawn_team_blue.button_pressed = true team_row.add_child(spawn_team_blue) team_row.add_child(spawn_team_red) spawn_radius_label = _field_label("Zonen-Radius: 6 m") spawn_panel.add_child(spawn_radius_label) spawn_radius_slider = HSlider.new() spawn_radius_slider.min_value = SpawnOps.MIN_ZONE_RADIUS spawn_radius_slider.max_value = SpawnOps.MAX_ZONE_RADIUS spawn_radius_slider.step = 1.0 spawn_radius_slider.value = SpawnOps.DEFAULT_ZONE_RADIUS spawn_radius_slider.custom_minimum_size = Vector2(0, 32) spawn_radius_slider.value_changed.connect(_on_spawn_radius_changed) spawn_panel.add_child(spawn_radius_slider) spawn_sel_label = Label.new() spawn_sel_label.text = "Keine Zone ausgewählt." spawn_sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART spawn_panel.add_child(spawn_sel_label) spawn_delete_btn = Button.new() spawn_delete_btn.text = "Zone löschen (Entf)" spawn_delete_btn.custom_minimum_size = Vector2(0, 40) spawn_delete_btn.tooltip_text = "Die ausgewählte Spawn-Zone löschen (Strg+Z holt sie zurück)" spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed) spawn_panel.add_child(spawn_delete_btn) spawn_panel.add_child(_muted_label( "Klick setzt eine Zone, Klick+Ziehen verschiebt sie. Jede Zone ist " + "EIN Rückgängig-Schritt (Strg+Z). ESC beendet das Spawn-Werkzeug.")) # --- Bauen (Karte t_97ea59c5): Modular-Builder-Pieces im App-Skelett --- vbox.add_child(_separator()) build_edit_btn = Button.new() build_edit_btn.text = "🏠 Bauen (Häuser)" build_edit_btn.custom_minimum_size = Vector2(0, 48) build_edit_btn.toggle_mode = true build_edit_btn.tooltip_text = "Fundamente, Wände, Dächer, Treppen und Türen bauen — im 2-Meter-Raster, wie Bauklötze" build_edit_btn.pressed.connect(_on_build_btn_pressed) vbox.add_child(build_edit_btn) build_panel = VBoxContainer.new() build_panel.add_theme_constant_override("separation", 6) build_panel.visible = false vbox.add_child(build_panel) build_panel.add_child(_field_label("Bauteil:")) var build_grid := GridContainer.new() build_grid.columns = 2 build_grid.add_theme_constant_override("h_separation", 6) build_grid.add_theme_constant_override("v_separation", 6) build_panel.add_child(build_grid) var build_group := ButtonGroup.new() for kind_id in BuildOps.KINDS.keys(): var kind_btn := Button.new() kind_btn.text = str(BuildOps.KINDS[kind_id]["label"]) kind_btn.toggle_mode = true kind_btn.button_group = build_group kind_btn.custom_minimum_size = Vector2(148, 44) kind_btn.clip_text = true kind_btn.tooltip_text = "%s — %s" % [str(BuildOps.KINDS[kind_id]["label"]), str(BuildOps.KINDS[kind_id].get("info", ""))] kind_btn.pressed.connect(_on_build_kind_pressed.bind(str(kind_id))) build_kind_buttons[str(kind_id)] = kind_btn build_grid.add_child(kind_btn) (build_kind_buttons["fundament"] as Button).set_pressed_no_signal(true) build_panel.add_child(_muted_label( "Linksklick setzt das Bauteil aufs 2-m-Raster — auch AUF andere Teile " + "(Wände aufs Fundament stapeln, Dach auf die Wände). Jedes Bauteil ist " + "EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Bauen-Modus.")) # Signale (Änderungen → Dokument + Ansicht) prop_name.text_changed.connect(_on_prop_changed.bind("name")) prop_author.text_changed.connect(_on_prop_changed.bind("author")) prop_desc.text_changed.connect(_on_prop_changed.bind("description")) prop_version.value_changed.connect(_on_version_changed) prop_weather.item_selected.connect(_on_weather_changed) return panel func _sel_op_button(text: String, tip: String, action: Callable) -> Button: var btn := Button.new() btn.text = text btn.custom_minimum_size = Vector2(140, 40) btn.tooltip_text = tip btn.pressed.connect(action) return btn func _header_label(text: String) -> Label: var label := Label.new() label.text = text label.add_theme_font_size_override("font_size", 19) label.add_theme_color_override("font_color", Color(0.55, 0.8, 1.0)) return label func _field_label(text: String) -> Label: var label := Label.new() label.text = text label.modulate = Color(1, 1, 1, 0.7) return label func _muted_label(text: String) -> Label: var label := Label.new() label.text = text label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.modulate = Color(1, 1, 1, 0.55) return label func _separator() -> HSeparator: return HSeparator.new() # ------------------------------------------------------------ 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 new_dialog.popup_centered() 1: # Öffnen open_dialog.popup_centered() 2: # Speichern _save() 3: # Speichern unter save_dialog.popup_centered() 4: # Beenden get_tree().quit() 5: # Karte testen _on_test_run() ## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!) ## nach Temp exportieren und den scopedvr-client als EIGENES Fenster starten. ## Der Editor bleibt offen; das Spiel beendet sich selbst sauber (ESC). func _on_test_run() -> void: if doc.is_empty(): _set_status("Erst eine Map anlegen (Datei → Neue Map), dann testen.", true) return var cfg := TestRun.load_cfg() var client_dir := TestRun.find_client(ProjectSettings.globalize_path("res://"), cfg) if client_dir.is_empty(): _show_errors("Karte testen — das Spiel wurde nicht gefunden:", [ "Der ScopedVR-Client (Ordner „scopedvr-client“) wurde nicht neben dem Editor gefunden.", "So geht es: Den Client-Ordner neben den Editor-Ordner legen (gleicher Überordner),", "ODER in der Datei scopedvr_editor.cfg im Ordner „ScopedVR Map Editor“ (Godot-App-Daten) eintragen:", " [test]", " client_dir=\"/pfad/zu/scopedvr-client\"", " godot_path=\"/pfad/zu/godot\" (nur nötig, wenn der Editor NICHT über Godot gestartet wird)", ]) _set_status("Karte testen: Spiel nicht gefunden — siehe Fenster.", true) return var godot := TestRun.find_godot(cfg) if godot.is_empty(): _show_errors("Karte testen — Godot-Programm nicht gefunden:", [ "Der Editor läuft gerade nicht über ein Godot-Programm, deshalb weiß er nicht,", "wie er das Spiel-Fenster starten soll. Trage in scopedvr_editor.cfg ein:", " [test]", " godot_path=\"/pfad/zu/godot\"", ]) _set_status("Karte testen: Godot-Programm nicht gefunden — siehe Fenster.", true) return var exported := TestRun.export_temp_map(doc) if not bool(exported["ok"]): _show_errors("Karte testen — die Map ist noch nicht lauffähig:", exported["errors"]) _set_status("Testlauf nicht möglich — siehe Fenster für die fehlenden Punkte (z. B. Spawn-Zonen).", true) return var map_name := str(doc.get("info", {}).get("name", "?")) var args := TestRun.launch_args(client_dir, str(exported["path"])) var pid := OS.create_process(godot, args) if pid <= 0: _show_errors("Karte testen — Start fehlgeschlagen:", [ "Das Spiel-Fenster konnte nicht gestartet werden (Programm: %s)." % godot]) _set_status("Karte testen: Start fehlgeschlagen.", true) return _set_status("▶ Karte „%s“ läuft im Test-Fenster — WASD laufen, ESC zurück zum Editor. Der Editor bleibt offen." % map_name) 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() _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() 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 _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() # --------------------------------------------------------------- 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()) ## Rechtsklick-Filter der Kamera: während des Platzierens gehört die rechte ## Maustaste dem Ghost-Abbruch (interaction) — der Flug startet dann nicht. func _fly_rmb_blocked() -> bool: return interaction.placing 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() match key: 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: _save() 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: # 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.") "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") 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 ------ func _on_library_pressed(oid: String) -> void: if tool_mode != "objects": _set_tool_mode("objects") # Bibliothek-Klick wechselt automatisch zurück if interaction.placing and interaction.place_oid == oid: # Nochmal geklickt → Modus beenden (An/Aus ohne Umwege). interaction.cancel_placement() _set_status("Platzieren abgebrochen.") return for other_oid in library_buttons: (library_buttons[other_oid] as Button).button_pressed = false library_buttons[oid].button_pressed = true interaction.start_placement(oid) ## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf. func _highlight_library(oid: String) -> void: for other_oid in library_buttons: var btn := library_buttons[other_oid] as Button btn.button_pressed = other_oid == oid btn.modulate = Color(1, 1, 1) if oid == other_oid else Color(1, 1, 1, 0.85) 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) func _terrain_mode_button(text: String, group: ButtonGroup) -> Button: var btn := Button.new() btn.text = text btn.toggle_mode = true btn.button_group = group btn.custom_minimum_size = Vector2(0, 40) return btn func _on_terrain_btn_pressed() -> void: if 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() return if mode == "terrain": _leave_spawn_mode() _leave_build_mode() _enter_terrain_mode() elif mode == "spawns": _leave_terrain_mode() _leave_build_mode() _enter_spawn_mode() elif mode == "bauen": _leave_terrain_mode() _leave_spawn_mode() _enter_build_mode() else: _leave_terrain_mode() _leave_spawn_mode() _leave_build_mode() _enter_objects_mode() 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_panel.visible = true help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Rechtsklick-Ziehen: Gegenteil / Gras zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Mittlere Maustaste: Kamera · Strg+Z: letzter Strich weg" terrain_edit_btn.button_pressed = true terrain_tool.call("set_brush_mode", _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_panel != null: terrain_panel.visible = false _sync_terrain_button() func _enter_spawn_mode() -> void: interaction.call("cancel_placement") interaction.call("clear_selection", true) tool_mode = "spawns" 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" spawn_edit_btn.button_pressed = true spawn_tool.call("activate") spawn_tool.call("set_team", _selected_spawn_team()) spawn_tool.call("set_radius", float(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(_selected_spawn_team())) func _leave_spawn_mode() -> void: if spawn_tool != null: spawn_tool.call("deactivate") if spawn_panel != null: 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.) 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 _selected_terrain_mode() -> int: if terrain_mode_lower.button_pressed: return TerrainField.Mode.LOWER if terrain_mode_smooth.button_pressed: return TerrainField.Mode.SMOOTH if terrain_mode_paint.button_pressed: return TerrainField.Mode.PAINT return TerrainField.Mode.RAISE func _sync_terrain_button() -> void: terrain_edit_btn.button_pressed = tool_mode == "terrain" ## Mode-Buttons (ButtonGroup) → Pinsel-Modus; Palette nur im Malen-Modus. func _on_terrain_mode_changed(_pressed: bool) -> void: if terrain_palette != null: terrain_palette.visible = _selected_terrain_mode() == TerrainField.Mode.PAINT if terrain_tool != null: terrain_tool.call("set_brush_mode", _selected_terrain_mode()) ## Material-Palette (Karte t_603ec1a8): gewähltes Material an den Pinsel. func _on_terrain_material_changed(pressed: bool, mat_id: int) -> void: if pressed and terrain_tool != null: terrain_tool.call("set_paint_material", mat_id) ## Pinselgrößen-Presets (Karte t_603ec1a8): setzen den Radius-Slider ## (der Label-Text + Ring-Optik hängen am value_changed-Signal). func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button: var btn := Button.new() btn.text = text btn.toggle_mode = true btn.button_group = group btn.custom_minimum_size = Vector2(0, 48) btn.tooltip_text = "Pinselradius %d m" % int(radius_m) btn.toggled.connect(_on_terrain_size_preset.bind(radius_m)) return btn func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void: # Null-Check als Schutznetz: beim Panel-Bau existiert der Slider noch # nicht (Programmatic-Pressed darf den Handler nie früh treffen). if pressed and terrain_radius_slider != null: terrain_radius_slider.value = radius_m ## Terrain-Pinsel-Größe per [ / ] (Karte t_55257792): durch die 3 Presets ## Klein/Mittel/Groß zyklisch — der Preset-Button wird mit gesetzt (set_pressed ## feuert toggled → Slider + Label + Pinssel-Ring wandern mit). func _terrain_brush_size_step(up: bool) -> void: if terrain_radius_slider == null: return var next := MoveMath.brush_next_preset(float(terrain_radius_slider.value), up) var btn: Button = null match next: 4.0: btn = terrain_size_btn_small 12.0: btn = terrain_size_btn_medium 32.0: btn = terrain_size_btn_large if btn != null: btn.set_pressed(true) # feuert _on_terrain_size_preset → Slider/Label else: terrain_radius_slider.value = next terrain_tool.call("set_radius", next) _set_status("Pinsel-Größe: %d m (mit [ / ] durch Klein/Mittel/Groß zyklisch)." % int(next)) ## Terrain-Pinsel-Modus per Ziffer 1–4 (Karte t_55257792): Heben/Senken/ ## Glätten/Malen — der Modus-Button wird mit gesetzt (toggled feuert die ## bestehende Sync-Kette inkl. Material-Palette im Malen-Modus). Malen startet ## mit der zuletzt gewählten Materialfarbe (paint_material bleibt stehen). func _terrain_brush_mode_key(mode_idx: int) -> void: var btn: Button = null match mode_idx: 0: btn = terrain_mode_raise 1: btn = terrain_mode_lower 2: btn = terrain_mode_smooth 3: btn = 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]) func _on_terrain_radius_changed(value: float) -> void: terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value) if terrain_tool != null: terrain_tool.call("set_radius", value) func _on_terrain_strength_changed(value: float) -> void: terrain_strength_label.text = "Stärke: %.1f m pro Sekunde" % value if terrain_tool != null: terrain_tool.call("set_strength", value) ## 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_panel.visible = false _sync_terrain_button() 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 --- # (Karte t_c4162c5d — Spawn-Zonen malen, Live-Check, Map-Fertig-Check) func _spawn_team_button(text: String, group: ButtonGroup, team: int) -> Button: var btn := Button.new() btn.text = text btn.toggle_mode = true btn.button_group = group btn.custom_minimum_size = Vector2(0, 40) btn.pressed.connect(_on_spawn_team_changed.bind(team)) return btn func _on_spawn_btn_pressed() -> void: if spawn_edit_btn.button_pressed: _set_tool_mode("spawns") else: _set_tool_mode("objects") func _selected_spawn_team() -> int: return 1 if spawn_team_red.button_pressed else 0 func _sync_spawn_button() -> void: spawn_edit_btn.button_pressed = tool_mode == "spawns" func _on_spawn_team_changed(team: int) -> void: if spawn_tool != null and tool_mode == "spawns": spawn_tool.call("set_team", team) func _on_spawn_radius_changed(value: float) -> void: spawn_radius_label.text = "Zonen-Radius: %d m" % int(value) if spawn_tool != null and tool_mode == "spawns": spawn_tool.call("set_radius", value) func _on_spawn_delete_pressed() -> void: if tool_mode == "spawns": spawn_tool.call("delete_selected") ## Panel-Zeile „Welche Zone ist angewählt?“ — WIRD vom Spawn-Tool gerufen. func _sync_spawn_selection() -> void: if spawn_tool == null: return var team: int = spawn_tool.get("selected_team") var pid: String = str(spawn_tool.get("selected_id")) if team < 0 or pid == "": spawn_sel_label.text = "Keine Zone ausgewählt.\nKlicke im Viewport auf einen Kreis." spawn_delete_btn.disabled = true return spawn_sel_label.text = "Ausgewählt: %s (%s)\nVerschieben: klicken+ziehen · Radius: Regler oben" % [ pid, SpawnOps.team_label(team)] spawn_delete_btn.disabled = false ## LIVE-Validierung (Loader-Regeln) — immer sichtbar, nach JEDEM Struktur-Wechsel. func _refresh_spawn_status() -> void: if doc.is_empty() or spawn_status == null: return spawn_status.text = SpawnOps.live_status(doc, manifest) ## 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(check_btn) _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) ## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt. ## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl, ## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln ## sich in die Viewport-Auswahl (Baum und Szene sind immer synchron). func _on_tree_selected() -> void: if _tree_select_lock: return var item := content_tree.get_selected() if item == null: return if item.has_meta("gebaeude_index"): _on_tree_selected_gebaeude(int(item.get_meta("gebaeude_index"))) return var indices: Array = [] var walk := content_tree.get_next_selected(null) while walk != null: if walk.has_meta("obj_index"): indices.append(int(walk.get_meta("obj_index"))) walk = content_tree.get_next_selected(walk) if indices.is_empty(): return var objects: Array = doc.get("objects", []) var valid: Array = [] for idx in indices: if idx >= 0 and idx < objects.size(): valid.append(idx) if valid.is_empty(): return if valid.size() == 1: interaction.select_object(valid[0], true) _set_status("%s ausgewählt — ziehen zum Verschieben, R/Mausrad zum Drehen, Entf zum Löschen. Strg+Klick markiert weitere dazu." % object_label(str(objects[valid[0]].get("id", "")))) else: interaction.select_many(valid) _set_status("%d Objekte im Baum markiert — Entf löscht ALLE, Strg+D dupliziert ALLE (jeweils EIN Undo-Schritt)." % valid.size()) ## Baum: Bauteil-Klick → Bauen-Modus + Teil anwählen (Karte t_97ea59c5). func _on_tree_selected_gebaeude(gi: int) -> void: if tool_mode != "bauen": _set_tool_mode("bauen") build_tool.call("select_index", gi) ## Auswahl (aus dem Viewport) im Baum mitmarkieren — MEHRFACH-fähig ## (Karte t_c46d31c2): ALLE gewählten Objekt-Zeilen werden selektiert, Zeilen, ## die nicht mehr gewählt sind, werden abgewählt. Primär-Objekt bleibt sichtbar. func _mark_tree_selection(idx: int) -> void: _mark_tree_selection_multi([idx]) func _mark_tree_selection_multi(indices: Array) -> void: _tree_select_lock = true var wanted := {} for i in indices: wanted[int(i)] = true _mark_tree_selection_walk(content_tree.get_root(), wanted) var primary: Variant = interaction.get("selected_index") if interaction != null else -1 if primary is int and primary >= 0: _ensure_tree_visible(content_tree.get_root(), primary) _tree_select_lock = false func _mark_tree_selection_walk(item: TreeItem, wanted: Dictionary) -> void: if item == null: return if item.has_meta("obj_index"): var idx := int(item.get_meta("obj_index")) if wanted.has(idx): item.select(0) elif item.is_selected(0): item.deselect(0) for child in item.get_children(): _mark_tree_selection_walk(child, wanted) func _ensure_tree_visible(item: TreeItem, idx: int) -> void: if item == null: return if item.has_meta("obj_index") and int(item.get_meta("obj_index")) == idx: content_tree.ensure_visible(item) return for child in item.get_children(): _ensure_tree_visible(child, idx) ## Rechte Panel-Sektion synchronisieren OHNE Statuszeilen-Text zu ändern ## (sonst überschreibt ein Stummsync die gerade gesetzte Hilfs-Meldung). ## Mehrfachauswahl/Gruppen-fähig (Karte t_f6f29d86): Panel listet ALLE ## gewählten Objekte auf + zeigt die Gruppen-Bindung an. func _sync_selection_panel() -> void: var objects: Array = doc.get("objects", []) var sel: Array = interaction.selected_indices var valid: Array = [] for i in sel: if i is int and i >= 0 and i < objects.size(): valid.append(i) var has_sel := not valid.is_empty() for btn in [sel_rotate_btn, sel_delete_btn, sel_dup_btn, sel_mirror_x_btn, sel_mirror_z_btn, sel_raise_btn, sel_lower_btn]: btn.disabled = not has_sel var has_group := false for i in valid: if GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED: has_group = true break sel_group_btn.disabled = valid.size() < 2 sel_ungroup_btn.disabled = not has_group if not has_sel: sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." return if valid.size() == 1: var idx: int = valid[0] var obj: Dictionary = objects[idx] var pos: Array = obj.get("pos", [0.0, 0.0, 0.0]) var lift := ObjectOps.lift_offset(doc, idx) var lift_text := "am Boden" if absf(lift) < 0.02 else ("%+.2f m über dem Boden" % lift) var group_line := "" var gid := GroupOps.group_id(obj) if gid != GroupOps.UNGROUPED: group_line = "\nGruppe %d — zieht & dreht GEMEINSAM (Lösen: Button unten)" % (gid + 1) sel_label.text = "%s (Objekt %d von %d)\nPosition: %.0f, %.0f m · Höhe: %s · Richtung: %.0f°%s\nZiehen: verschieben · R oder Mausrad: drehen (15°)\nBild↑/↓: anheben/absenken · Strg+D: duplizieren\nSpiegeln/Löschen: Buttons oben · Strg+Z: zurück" % [ object_label(str(obj.get("id", ""))), idx + 1, objects.size(), float(pos[0]), float(pos[2]), lift_text, float(obj.get("rot_y_deg", 0.0)), group_line] return # Mehrfachauswahl: Liste aller gewählten Objekte (max. 8 Zeilen, dann „…“). var names := PackedStringArray() for i in valid: if names.size() < 8: names.append(object_label(str((objects[i] as Dictionary).get("id", "?")))) var more := "" if valid.size() > 8: more = "\n… und %d weitere" % (valid.size() - 8) sel_label.text = "%d Objekte ausgewählt:\n%s%s\nGemeinsam: ziehen (Gruppe wandert zusammen), drehen um die Mitte, Entf löscht alle." % [ valid.size(), " · ".join(names), more] ## Hilfs-Meldung zur frischen Auswahl (Klartext + Budget-Warnung + nächster Schritt). func _selection_status(idx: int) -> void: var objects: Array = doc.get("objects", []) var sel: Array = interaction.selected_indices if (idx < 0 or idx >= objects.size()) and sel.is_empty(): _base_tool_status() return var used := int(SvmapDocument.poly_usage(doc, manifest)) var budget := int(SvmapIO.MAX_TOTAL_POLY) var budget_warning := "" if used > budget * 80 / 100: budget_warning = " ⚠ Poly-Budget zu %.0f %% voll — bald passt kein Objekt mehr rein." % (100.0 * used / budget) if sel.size() > 1: _set_status("%d Objekte ausgewählt — ziehen bewegt ALLE gemeinsam, R/Mausrad dreht sie um die Mitte, Entf löscht alle, Strg+G gruppiert.%s" % [sel.size(), budget_warning]) return if idx < 0 or idx >= objects.size(): _base_tool_status() return var obj: Dictionary = objects[idx] var gid := GroupOps.group_id(obj) var group_hint := "" if gid != GroupOps.UNGROUPED: group_hint = " (Gruppe %d — die zieht GEMEINSAM)" % (gid + 1) _set_status("%s%s ausgewählt — ziehen zum Verschieben, R oder Mausrad zum Drehen, Bild↑/↓ anheben/absenken, Strg+D duplizieren, Entf zum Löschen.%s" % [ object_label(str(obj.get("id", ""))), group_hint, budget_warning]) # ------------------------------------------------------- Eigenschaften ----- func _on_prop_changed(text: String, field: String) -> void: if _loading_ui_values or doc.is_empty(): return var info: Dictionary = doc["info"] match field: "name": info["name"] = text "author": info["author"] = text "description": info["description"] = text dirty = true _refresh_file_label() func _on_version_changed(value: float) -> void: if _loading_ui_values or doc.is_empty(): return doc["info"]["version"] = int(value) dirty = true _refresh_file_label() func _on_weather_changed(index: int) -> void: if _loading_ui_values or doc.is_empty(): return var weathers := ["mittag", "abend", "regen", "nacht"] doc["weather"] = weathers[index] dirty = true _refresh_file_label() # Licht/Stimmung im Viewport sofort anpassen view_builder.call("rebuild", doc, manifest) _sync_grid_to_builder() _set_status("Wetter geändert auf „%s“." % weathers[index]) # ------------------------------------------------------- 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() _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 var info: Dictionary = doc.get("info", {}) prop_name.text = str(info.get("name", "")) prop_author.text = str(info.get("author", "")) prop_desc.text = str(info.get("description", "")) prop_version.value = float(info.get("version", 1)) var weathers := ["mittag", "abend", "regen", "nacht"] var weather := str(doc.get("weather", "mittag")) prop_weather.select(maxi(0, weathers.find(weather))) var terrain: Dictionary = doc.get("terrain", {}) terrain_info.text = "Größe: %.0f × %.0f m\nAuflösung: %d × %d Höhenpunkte\nHöhenbereich: %.0f bis %.0f m" % [ float(terrain.get("size_x_m", 0.0)), float(terrain.get("size_z_m", 0.0)), int(terrain.get("resolution", 0)), int(terrain.get("resolution", 0)), float(terrain.get("min_height_m", 0.0)), float(terrain.get("max_height_m", 0.0))] _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() camera.call("frame_map", float(doc.get("terrain", {}).get("size_x_m", 1024.0))) # --- 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) func _refresh_tree() -> void: content_tree.clear() var root := content_tree.create_item() var objects: Array = doc.get("objects", []) var obj_item := content_tree.create_item(root) obj_item.set_text(0, "Objekte (%d)" % objects.size()) for i in objects.size(): var obj = objects[i] var item := content_tree.create_item(obj_item) if obj is Dictionary: item.set_text(0, "%s #%d" % [object_label(str(obj.get("id", "?"))), i + 1]) item.set_meta("obj_index", i) else: item.set_text(0, "ungültig #%d" % (i + 1)) var zones: Dictionary = doc.get("spawn_zones", {}) var spawn_item := content_tree.create_item(root) spawn_item.set_text(0, "Spawn-Zonen") for team_key in ["0", "1"]: if not zones.has(team_key): continue var zone: Dictionary = zones[team_key] var team_item := content_tree.create_item(spawn_item) var team_name := "Team Blau" if team_key == "0" else "Team Rot" team_item.set_text(0, "%s (%d Punkte)" % [team_name, (zone.get("points", []) as Array).size()]) for p in (zone.get("points", []) as Array): if p is Dictionary: var point_item := content_tree.create_item(team_item) point_item.set_text(0, "%s (%.0f, %.0f)" % [ str(p.get("id", "?")), float(p.get("pos", [0, 0, 0])[0]), float(p.get("pos", [0, 0, 0])[2])]) # Gebäude (Karte t_97ea59c5): Bauteile im Baum — Klick wählt das Teil an. var geb_list: Array = BuildOps.pieces(doc) if not geb_list.is_empty(): var geb_item := content_tree.create_item(root) geb_item.set_text(0, "Gebäude (%d Bauteile)" % geb_list.size()) for i in geb_list.size(): var teil: Dictionary = geb_list[i] var g_child := content_tree.create_item(geb_item) g_child.set_text(0, "%s #%d" % [BuildOps.kind_label(str(teil.get("id", "?"))), i + 1]) g_child.set_meta("gebaeude_index", i) 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)] # 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 ""] get_window().title = "%s — %s" % [APP_TITLE, map_name] func _set_status(text: String, is_error := false) -> void: status_label.text = text status_label.add_theme_color_override("font_color", Color(1.0, 0.6, 0.5) if is_error else Color(0.75, 0.95, 0.75)) 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 # ---------------------------------------------------- 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 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") # ------------------------------------------------ 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_radius_slider.value = 45.0 terrain_strength_slider.value = 10.0 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_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 (Rechtsklick-Ziehen senkt, ohne umzuschalten).") await _demo_shot(out_dir + "/terrain_lower.png") # 3) Hügelkuppe glätten (blauer Ring). 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_mode_raise.button_pressed = true 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") 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