- R = Auswahl 90° drehen (Szene um Auswahl-Mitte, Bauen-Bauteil um Fußpunkt, Gruppe komplett; Umschalt+R = -90°), Mausrad-Feindrehen 15° bleibt - Pfeiltasten = 2-m-Raster-Verschiebung, Richtung = Kamera-Blickrichtung auf nächste Weltachse gerastet (MoveMath.nudge_delta, pure) - Bild↑/↓ = ±0,25 m auf der GANZEN Auswahl (EIN Undo), Strg+Bild↑ = Fußpunkt aufs Terrain/Boden absetzen - G = Greifen (Blender-Semantik): Auswahl hängt an der Maus, LMB setzt mit Raster-Snap, Esc rollt byte-genau zurück (ShortcutPolicy cancel_grab) - Terrain-Pinsel: [ / ] = Preset-Zyklus Klein/Mittel/Groß (Slider/Label/Preset-Buttons wandern mit), 1-4 = Heben/Senken/Glätten/Malen (PAINT-Clamp-Fix, Palette mit Malen sichtbar); Ziffern NUR im Terrain-Werkzeug - Strg halten = temporärer Snap-Zwang beim Bewegen/Greifen/Platzieren - Tests: test_shortcuts.gd 157 Checks (R byte-genau, Pfeil-Raster, Lift-Stufen, Boden-Snap, Greifen+Esc, Pinsel-Zyklus, History-Eintrag pro Operation), test_gui_smoke.gd erweitert (38 Checks) und in run_tests.sh aufgenommen; F-Regression seit Karte 1 damit abgedeckt - Suite 9/9 grün (696 Checks), Builder-Export 21/0, Parse-Check 11 Dateien grün
2369 lines
No EOL
94 KiB
GDScript
2369 lines
No EOL
94 KiB
GDScript
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 |