scopedvr-map-core/editor/library_browser.gd
2026-11-01 19:31:33 +01:00

167 lines
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, 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)
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)