Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
8.7 KiB
GDScript
215 lines
8.7 KiB
GDScript
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)
|