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 (IDs sind der Vertrag, Entscheidung 20) const NAMES := { "crate_wood": "Holzkiste", "crate_metal": "Metallkiste", "wall_concrete": "Betonmauer", "barrier_sandbag": "Sandsack-Sperre", "bunker_concrete": "Bunker", "watchtower_wood": "Wachturm", "fence_metal": "Zaun", "rock_large": "Großer Felsen", "boulder_small": "Kleiner Felsen", "tree_pine": "Nadelbaum", "bush_small": "Strauch", "bruecke_holz": "Brücke (Holz)", "hoehleneingang": "Höhleneingang", "felsbogen": "Felsbogen", # Natur-Update (Karte t_7cf80244): kuratierte CC0-Packs (Quaternius # MegaKit, Kyle's Rock/Mushroom) — Namen aus stage/kuration.json. "baum_kiefer_1": "Kiefer (schlank)", "baum_kiefer_2": "Kiefer (kräftig)", "baum_kiefer_3": "Kiefer (mittel)", "baum_kiefer_4": "Kiefer (hoch)", "baum_laub_1": "Laubbaum (rund)", "baum_laub_2": "Laubbaum (breit)", "baum_laub_3": "Laubbaum (schlank)", "baum_laub_4": "Laubbaum (klein)", "baum_tot_1": "Totholz-Baum", "fels_moos_block_gross": "Moosfelsen (groß)", "fels_moos_block_hoch": "Moosfelsen (hoch)", "fels_moos_platte": "Moosfelsen (Platte)", "fels_moos_scharte": "Moosfelsen (klein)", "fels_trocken_block": "Trockenfelsen (hoch)", "fels_trocken_platte": "Trockenfelsen (Platte)", "fels_trocken_klein": "Trockenfelsen (klein)", "stein_mittel_1": "Großer Stein 1", "stein_mittel_2": "Großer Stein 2", "stein_kiesel_1": "Kieselstein", "strauch_gruen": "Strauch", "strauch_blumen": "Blühender Strauch", "pflanze_farn": "Farn", "pflanze_klee": "Klee-Pflanze", "blume_gross_1": "Blume (weiß)", "blume_gross_2": "Blume (hoch)", "blumen_strauss": "Blumenstrauß", "pflanze_niedrig": "Bodendecker", "pflanze_teppich": "Moospolster", "pflanze_teppich_gross": "Moospolster (groß)", "gras_kurz": "Kurzes Gras", "gras_hoch": "Hohes Gras", "gras_bueschel": "Grasbüschel", "gras_wispy_hoch": "Wispy-Gras (hoch)", "pilz_fliegenpilz": "Fliegenpilz", "pilz_pfifferling": "Pfifferling", "pilz_steinpilz": "Steinpilz", "pilz_shiitake": "Shiitake", "pilz_morchel": "Morchel", "pilz_austernseitling": "Austernseitling", } ## Gruppen für den Bibliothek-Browser (Anzeige-Reihenfolge = Reihenfolge hier). ## Natur-Update (t_7cf80244): die alte Gruppe "natur" ist in thematische ## Gruppen aufgelöst (die 4 Alt-Objekte wanderten mit). const GROUP_ORDER := ["kisten", "bauwerke", "bäume", "felsen", "pflanzen", "gras", "pilze", "sonstiges"] const GROUP_LABELS := { "kisten": "Kisten & Barrieren", "bauwerke": "Bauwerke", "bäume": "Bäume", "felsen": "Felsen", "pflanzen": "Pflanzen", "gras": "Gras", "pilze": "Pilze", "sonstiges": "Sonstiges", } const GROUP_IDS := { "kisten": ["crate_wood", "crate_metal", "barrier_sandbag", "wall_concrete"], "bauwerke": ["bunker_concrete", "watchtower_wood", "fence_metal", "bruecke_holz", "hoehleneingang", "felsbogen"], "bäume": ["tree_pine", "baum_kiefer_1", "baum_kiefer_2", "baum_kiefer_3", "baum_kiefer_4", "baum_laub_1", "baum_laub_2", "baum_laub_3", "baum_laub_4", "baum_tot_1"], "felsen": ["rock_large", "boulder_small", "fels_moos_block_gross", "fels_moos_block_hoch", "fels_moos_platte", "fels_moos_scharte", "fels_trocken_block", "fels_trocken_platte", "fels_trocken_klein", "stein_mittel_1", "stein_mittel_2", "stein_kiesel_1"], "pflanzen": ["bush_small", "strauch_gruen", "strauch_blumen", "pflanze_farn", "pflanze_klee", "blume_gross_1", "blume_gross_2", "blumen_strauss", "pflanze_niedrig", "pflanze_teppich", "pflanze_teppich_gross"], "gras": ["gras_kurz", "gras_hoch", "gras_bueschel", "gras_wispy_hoch"], "pilze": ["pilz_fliegenpilz", "pilz_pfifferling", "pilz_steinpilz", "pilz_shiitake", "pilz_morchel", "pilz_austernseitling"], } ## Karten-Übersicht (t_59fc0f21): Die feste Gruppenliste oben deckt nur die ## kuratierten Alt-/Natur-Objekte ab — der 347-Objekt-Katalog bringt seine ## eigene Kategorie mit (Manifest-Feld `kategorie`, siehe library/objects.json). ## Diese Zuordnung zeigt Pack-/Pipeline-Objekte in sinnvollen Gruppen statt ## alles in einer Mega-„Sonstiges"-Gruppe abzulegen (das war der eigentliche ## Übersichts-Killer). Reihenfolge = Anzeige-Reihenfolge nach GROUP_ORDER. const KAT_ORDER := ["mauern", "barrieren", "gebaeude", "requisiten", "fahrzeuge", "natur_uebrig"] const KAT_GROUPS := { "walls": {"key": "mauern", "label": "Mauern"}, "barriers": {"key": "barrieren", "label": "Barrieren"}, "buildings": {"key": "gebaeude", "label": "Gebäude"}, "props": {"key": "requisiten", "label": "Requisiten"}, "vehicles": {"key": "fahrzeuge", "label": "Fahrzeuge"}, "nature": {"key": "natur_uebrig", "label": "Natur (übrige)"}, } ## Anzeige-Größe der Vorschaubilder im Browser (Karte t_59fc0f21): vorher 40 px, ## jetzt Kacheln mit 96 px (128-px-PNGs werden auf die Anzeige-Größe skaliert). const THUMB_DISPLAY_PX := 96 static func object_label(oid: String) -> String: return NAMES.get(oid, oid.capitalize()) ## Gruppe einer ID (Objekte, die keine festen Gruppe haben → "sonstiges"). static func group_of(oid: String) -> String: for key in GROUP_IDS: if (GROUP_IDS[key] as Array).has(oid): return str(key) return "sonstiges" ## Gruppe eines Katalog-Eintrags (t_59fc0f21): erst die festen Gruppen (oben), ## dann das Manifest-Feld `kategorie` (KAT_GROUPS), sonst "sonstiges". ## entry darf leer sein (z. B. fremde ID ohne Manifest-Eintrag). static func group_of_entry(oid: String, entry: Dictionary) -> String: var fixed := group_of(oid) if fixed != "sonstiges": return fixed var kat := str(entry.get("kategorie", "")) if KAT_GROUPS.has(kat): return str(KAT_GROUPS[kat]["key"]) return "sonstiges" ## Deutscher Anzeigename: Manifest-Feld `name_de` (Pipeline-Objekte), sonst ## NAMES/fallback.ALLE Pipeline-Objekte zeigen damit ihren echten Namen statt ## des großgeschriebenen IDs (Karte t_59fc0f21). 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 IDs, die es in ## der objects.json WIRKLICH gibt, landen in der Liste (und unbekannte, ## noch nicht gruppierte IDs wandern nach "Sonstiges" — neue Bibliotheks- ## Einträge brechen die UI also 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 out: Array = [] # Reihenfolge (t_59fc0f21): Kategorie-Gruppen zuerst (347-Objekt-Katalog ist # überwiegend Pipeline-Objekte — Mauern/Gebäude/Requisiten oben), danach die # kuratierten Alt-/Natur-Gruppen, "Sonstiges" immer ganz hinten. for key in KAT_ORDER + GROUP_ORDER: 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 ## Thumbnail-Pfad (128-px-PNG, tools/gen_thumbs.gd erzeugt sie einmalig). static func thumb_path(oid: String) -> String: return "res://library/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) 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)