extends Control ## Standalone Phase 4 review. F6, or run this scene with -- --capture. const STAGE: Script = preload("res://tools/model_gallery_stage.gd") const MANIFEST: String = "res://assets/phase4_manifest.json" const FACTIONS: Dictionary = { "all": "Alle Modelle", "glutpakt": "Glutpakt", "nietenkonvent": "Nietenkonvent", "nebelchor": "Nebelchor", "wurzelbund": "Wurzelbund", "neutral": "Söldner & Arbeiter", "environment": "Spielfeld & Saumkerne", "rift": "Risskreaturen" } const ROLES: Dictionary = { "melee": "Nahkampf", "tank": "Tank", "ranged": "Fernkampf", "artillery": "Artillerie", "support": "Unterstützung", "mercenary": "Söldner", "enemy": "Wellengegner", "worker": "Arbeiter", "lane": "Lane-Segment", "core": "Saumkern" } var _assets: Array[Dictionary] = [] var _filtered: Array[Dictionary] = [] var _icon_paths: Dictionary = {} var _faction_select: OptionButton var _asset_select: OptionButton var _title: Label var _details: Label var _icon: TextureRect var _reference_note: Label var _auto_rotate: CheckButton var _view: Dictionary = {} func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) if "--compare" in OS.get_cmdline_user_args(): var comparison_script: Script = load("res://tools/model_reference_sheet.gd") as Script add_child(comparison_script.new()) return if "--capture" in OS.get_cmdline_user_args(): var capture_script: Script = load("res://tools/model_contact_sheet.gd") as Script var capture: Node = capture_script.new() as Node add_child(capture) return _load_catalog() _build_interface() _filter_assets(0) func _load_catalog() -> void: if not FileAccess.file_exists(MANIFEST): push_error("Phase 4 manifest missing. Export and import models first.") return var data: Variant = JSON.parse_string(FileAccess.get_file_as_string(MANIFEST)) if data is Dictionary: for value: Variant in data.get("assets", []): if value is Dictionary: _assets.append(value) var icon_manifest: String = "res://assets/phase3_manifest.json" if FileAccess.file_exists(icon_manifest): var icons: Variant = JSON.parse_string(FileAccess.get_file_as_string(icon_manifest)) if icons is Dictionary: for value: Variant in icons.get("assets", []): if value is Dictionary: _icon_paths[str(value.get("id", ""))] = str(value.get("resource_path", "")) func _build_interface() -> void: var backdrop: ColorRect = ColorRect.new() backdrop.color = Color("11151b") backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) backdrop.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(backdrop) var margin: MarginContainer = MarginContainer.new() margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) for edge: String in ["left", "top", "right", "bottom"]: margin.add_theme_constant_override("margin_" + edge, 24) add_child(margin) var layout: VBoxContainer = VBoxContainer.new() layout.add_theme_constant_override("separation", 18) margin.add_child(layout) var heading: Label = _label("SAUMWACHT / 3D-WERKSTATT", 27, Color("ddc898")) layout.add_child(heading) layout.add_child(_label("%d Modelle · Glutpakt und Risskreaturen in hoher Detailstufe" % _assets.size(), 17, Color("a8b0bd"))) var body: HBoxContainer = HBoxContainer.new() body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 24) layout.add_child(body) var sidebar: VBoxContainer = VBoxContainer.new() sidebar.custom_minimum_size.x = 320 sidebar.add_theme_constant_override("separation", 14) body.add_child(sidebar) sidebar.add_child(_label("FRAKTION", 13, Color("c4b58f"))) _faction_select = OptionButton.new() _faction_select.custom_minimum_size.y = 42 _faction_select.add_theme_font_size_override("font_size", 18) for key: String in FACTIONS: _faction_select.add_item(FACTIONS[key]) _faction_select.set_item_metadata(_faction_select.item_count - 1, key) _faction_select.item_selected.connect(_filter_assets) sidebar.add_child(_faction_select) sidebar.add_child(_label("MODELL", 13, Color("c4b58f"))) _asset_select = OptionButton.new() _asset_select.custom_minimum_size.y = 42 _asset_select.add_theme_font_size_override("font_size", 17) _asset_select.fit_to_longest_item = false _asset_select.item_selected.connect(_select_asset) sidebar.add_child(_asset_select) _title = _label("", 24, Color("f1e8d6")) _title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART sidebar.add_child(_title) _details = _label("", 16, Color("b8bfca")) _details.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART sidebar.add_child(_details) _icon = TextureRect.new() _icon.custom_minimum_size = Vector2(256, 200) _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED _icon.size_flags_vertical = Control.SIZE_EXPAND_FILL sidebar.add_child(_icon) _reference_note = _label("Iconreferenz · Phase 3", 14, Color("8e99aa")) sidebar.add_child(_reference_note) _auto_rotate = CheckButton.new() _auto_rotate.text = "Automatisch drehen" _auto_rotate.add_theme_font_size_override("font_size", 16) sidebar.add_child(_auto_rotate) var reset: Button = Button.new() reset.text = "Ansicht zurücksetzen" reset.custom_minimum_size.y = 38 reset.pressed.connect(_reset_view) sidebar.add_child(reset) var preview_box: PanelContainer = PanelContainer.new() preview_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL preview_box.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_child(preview_box) _view = STAGE.create(preview_box, Vector2(520, 480)) var container: SubViewportContainer = _view["container"] as SubViewportContainer container.gui_input.connect(_on_preview_input) layout.add_child(_label("Ziehen: Modell drehen · Mausrad: Zoom · Originalmodelle bleiben unverändert", 15, Color("939dab"))) func _label(text: String, font_size: int, color: Color) -> Label: var result: Label = Label.new() result.text = text result.add_theme_font_size_override("font_size", font_size) result.add_theme_color_override("font_color", color) return result func _filter_assets(index: int) -> void: _filtered.clear() _asset_select.clear() var faction: String = str(_faction_select.get_item_metadata(index)) for asset: Dictionary in _assets: var matches: bool = faction == "all" or str(asset.get("faction", "")) == faction if faction == "environment": matches = str(asset.get("role", "")) in ["lane", "core"] if matches: _filtered.append(asset) _asset_select.add_item(str(asset["id"]).to_upper() + " · " + str(asset["name"])) if not _filtered.is_empty(): _asset_select.select(0) _select_asset(0) else: _title.text = "Keine Modelle vorhanden" _details.text = "Blender-Export und Godot-Import zuerst ausführen." _icon.texture = null func _select_asset(index: int) -> void: if index < 0 or index >= _filtered.size(): return var asset: Dictionary = _filtered[index] _title.text = str(asset["name"]) var role: String = str(asset.get("role", "")) var bounds: Dictionary = asset.get("bounds_godot", {}) var low: Array = bounds.get("min", [0, 0, 0]) var high: Array = bounds.get("max", [0, 0, 0]) var dimensions: Vector3 = Vector3(float(high[0])-float(low[0]), float(high[1])-float(low[1]), float(high[2])-float(low[2])) _details.text = "%s · %s\n%d Dreiecke\nB %.2f m · H %.2f m · T %.2f m" % [ str(asset["id"]).to_upper(), str(ROLES.get(role, role)), int(asset.get("triangles", 0)), dimensions.x, dimensions.y, dimensions.z] var icon_id: String = str(asset["id"]) if icon_id == "worker": icon_id = "worker_funkenleser" var icon_path: String = str(_icon_paths.get(icon_id, "")) _icon.texture = load(icon_path) as Texture2D if not icon_path.is_empty() else null _reference_note.visible = _icon.texture != null if not STAGE.show_asset(_view, asset): _details.text += "\nModell konnte nicht geladen werden." _reset_view() func _process(delta: float) -> void: if _auto_rotate != null and _auto_rotate.button_pressed and not _view.is_empty(): var turntable: Node3D = _view["turntable"] as Node3D turntable.rotate_y(delta * 0.38) func _reset_view() -> void: if _view.is_empty(): return var turntable: Node3D = _view["turntable"] as Node3D var camera: Camera3D = _view["camera"] as Camera3D turntable.rotation = Vector3.ZERO camera.size = 2.9 func _on_preview_input(event: InputEvent) -> void: var camera: Camera3D = _view["camera"] as Camera3D var turntable: Node3D = _view["turntable"] as Node3D if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: camera.size = clampf(camera.size * 0.90, 1.6, 5.5) elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: camera.size = clampf(camera.size * 1.10, 1.6, 5.5) if event is InputEventMouseMotion and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _auto_rotate.button_pressed = false turntable.rotate_y(event.relative.x * 0.008)