extends RefCounted class_name LibraryBrowser ## Objekt-Bibliothek des Editors (Karte t_3a998ae4): deutscher Name, Gruppen ## für den Bibliothek-Browser und Thumbnail-Laden (128-px-PNGs, einmalig per ## Headless-Render erzeugt — tools/gen_thumbs.gd). ## ## Bewusst OHNE UI: reine Daten/Funktionen, damit die Tests (tests/test_library.gd) ## headless alles prüfen können. Die IDs sind der Vertrag (Entscheidung 20) — ## Gruppen/Namen leben HIER im Editor (die objects.json bleibt 1:1 die Kopie ## der Server-Registry, damit ein Sync sie nicht verwüstet). ## Deutsche Anzeige-Namen für Bibliotheks-IDs, die KEIN `name_de` im Manifest ## haben (IDs sind der Vertrag, Entscheidung 20). Seit der Military-Pack- ## Umstellung (25.10.2026) hat jedes Objekt ein `name_de` — die Liste bleibt ## leer und dient nur als Override-Möglichkeit. const NAMES := {} ## Fallback-Gruppe für Objekte ohne bekannte Kategorie. const GROUP_ORDER := ["sonstiges"] const GROUP_LABELS := { "sonstiges": "Sonstiges", } ## Kategorien des Military-Desert-Packs (Manifest-Feld `kategorie`, siehe ## library/objects.json; Quelle der Slugs/Namen: scopedvr-match-server ## tools/pack_katalog.py). Reihenfolge = Anzeige-Reihenfolge. Unbekannte ## Kategorien (z. B. spätere Bauteil-Module) tauchen automatisch mit ihrem ## Slug als Gruppe auf, ganz vor "Sonstiges". const KAT_ORDER := ["bauteile", "panzer", "radfahrzeuge", "luftfahrzeuge", "stellungen", "fahrzeugteile", "gebaeude", "zelte", "tuerme", "mauern", "sperren", "kisten", "technik", "ausruestung", "training", "baeume", "pflanzen", "felsen", "boden", "decals"] const KAT_GROUPS := { "bauteile": {"key": "bauteile", "label": "Bauteile (eigene Häuser)"}, "panzer": {"key": "panzer", "label": "Panzer & Transportpanzer"}, "radfahrzeuge": {"key": "radfahrzeuge", "label": "Radfahrzeuge"}, "luftfahrzeuge": {"key": "luftfahrzeuge", "label": "Flugzeuge & Hubschrauber"}, "stellungen": {"key": "stellungen", "label": "Geschütze & Stellungen"}, "fahrzeugteile": {"key": "fahrzeugteile", "label": "Fahrzeugteile"}, "gebaeude": {"key": "gebaeude", "label": "Gebäude & Hangars"}, "zelte": {"key": "zelte", "label": "Zelte"}, "tuerme": {"key": "tuerme", "label": "Türme & Masten"}, "mauern": {"key": "mauern", "label": "Mauern & Wände"}, "sperren": {"key": "sperren", "label": "Sperren & Zäune"}, "kisten": {"key": "kisten", "label": "Kisten, Fässer & Container"}, "technik": {"key": "technik", "label": "Technik & Funk"}, "ausruestung": {"key": "ausruestung", "label": "Ausrüstung & Zubehör"}, "training": {"key": "training", "label": "Training & Ziele"}, "baeume": {"key": "baeume", "label": "Bäume & Totholz"}, "pflanzen": {"key": "pflanzen", "label": "Pflanzen, Kakteen & Gras"}, "felsen": {"key": "felsen", "label": "Felsen & Steine"}, "boden": {"key": "boden", "label": "Boden, Straßen & Dünen"}, "decals": {"key": "decals", "label": "Boden-Decals"}, } ## Anzeige-Größe der Vorschaubilder im Browser (Karte t_59fc0f21): vorher 40 px, ## jetzt Kacheln mit 128 px (25.10.: größer, deutlicher) (128-px-PNGs werden auf die Anzeige-Größe skaliert). const THUMB_DISPLAY_PX := 128 ## Manifest-Cache (vom Editor beim Laden gesetzt): damit object_label() und ## group_of() auch ohne Manifest-Parameter den deutschen Namen / die Kategorie ## kennen (Statuszeile, Szenen-Baum, Inspector). static var _manifest_cache: Dictionary = {} static func setze_manifest(manifest: Dictionary) -> void: _manifest_cache = (manifest.get("objects", {}) as Dictionary) if manifest.has("objects") else {} static func object_label(oid: String) -> String: if BauKatalog.ist_bau(oid): return BauKatalog.label(oid) if NAMES.has(oid): return str(NAMES[oid]) var entry: Dictionary = _manifest_cache.get(oid, {}) as Dictionary var name_de := str(entry.get("name_de", "")) if not name_de.is_empty(): return name_de return oid.capitalize() ## Gruppe einer ID (ohne Manifest-Eintrag → "sonstiges"). static func group_of(oid: String) -> String: if BauKatalog.ist_bau(oid): return "bauteile" var entry: Dictionary = _manifest_cache.get(oid, {}) as Dictionary return group_of_entry(oid, entry) ## Gruppe eines Katalog-Eintrags: Manifest-Feld `kategorie` (KAT_GROUPS); ## unbekannte Kategorie → eigene Gruppe mit dem Slug als Schlüssel; kein ## Eintrag/keine Kategorie → "sonstiges". entry darf leer sein. static func group_of_entry(_oid: String, entry: Dictionary) -> String: var kat := str(entry.get("kategorie", "")) if kat.is_empty(): return "sonstiges" if KAT_GROUPS.has(kat): return str(KAT_GROUPS[kat]["key"]) return kat ## Deutscher Anzeigename: Manifest-Feld `name_de`, sonst NAMES/fallback. static func entry_label(oid: String, entry: Dictionary) -> String: var name_de := str(entry.get("name_de", "")) if not name_de.is_empty(): return name_de return object_label(oid) ## Gruppen für den Browser, geprüft gegen das Manifest: nur Gruppen mit ## Objekten erscheinen. Unbekannte Kategorien wandern als eigene Gruppe vor ## "Sonstiges" — neue Bibliotheks-Einträge brechen die UI nicht. static func groups_in_order(manifest: Dictionary) -> Array: var lib: Dictionary = manifest.get("objects", {}) var by_group := {} for oid in lib.keys(): var entry: Dictionary = lib.get(oid, {}) as Dictionary var key := group_of_entry(str(oid), entry) if not by_group.has(key): by_group[key] = [] (by_group[key] as Array).append(str(oid)) var reihenfolge: Array = KAT_ORDER.duplicate() var fremde: Array = [] for key in by_group.keys(): if not reihenfolge.has(key) and not GROUP_ORDER.has(key): fremde.append(key) fremde.sort() reihenfolge.append_array(fremde) reihenfolge.append_array(GROUP_ORDER) var out: Array = [] for key in reihenfolge: var ids: Array = (by_group.get(key, []) as Array) ids.sort() if ids.is_empty() and key != "sonstiges": continue out.append({"key": key, "label": _group_label(key), "ids": ids}) return out static func _group_label(key: String) -> String: if GROUP_LABELS.has(key): return str(GROUP_LABELS[key]) for kat in KAT_GROUPS.values(): if str(kat["key"]) == key: return str(kat["label"]) return key.capitalize() ## Thumbnail-Pfad (128-px-PNG, tools/gen_thumbs.gd erzeugt sie einmalig). static func thumb_path(oid: String) -> String: return "res://core/thumbs/%s.png" % oid ## Thumbnail als Textur laden — direkt über Image (kein Import-System nötig, ## geht damit auch frisch nach git pull ohne Import), auf Anzeige-Größe ## runtergerechnet. size_px steuert die Anzeige-Größe (Default = THUMB_DISPLAY_PX ## = 96, Karte t_59fc0f21; vorher fest 40). null = noch kein Thumbnail → ## Button fällt aufs Text-Layout zurück (Fehler sieht man nicht, man sieht ## einfach nur kein Bild). static func load_thumb(oid: String, size_px := THUMB_DISPLAY_PX) -> Texture2D: var path := thumb_path(oid) # In der exportierten App liegt nur die importierte Fassung. if OS.has_feature("android"): return load(path) as Texture2D if ResourceLoader.exists(path) else null if not FileAccess.file_exists(path): return null var img := Image.load_from_file(path) if img == null: return null if img.get_width() != size_px or img.get_height() != size_px: img.resize(size_px, size_px, Image.INTERPOLATE_LANCZOS) return ImageTexture.create_from_image(img)