scopedvr-map-editor/src/editor/library_browser.gd

216 lines
8.7 KiB
GDScript

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)