#!/usr/bin/env python3 """Build an offline comparison index and catalog from the current export manifest.""" from html import escape import json from pathlib import Path PROJECT = Path(__file__).resolve().parents[1] OUT = PROJECT / 'docs/screenshots/phase4' FACTIONS = {'glutpakt': 'Glutpakt', 'nietenkonvent': 'Nietenkonvent', 'nebelchor': 'Nebelchor', 'wurzelbund': 'Wurzelbund', 'neutral': 'Söldner & Arbeiter'} ROLES = {'melee': 'Nahkampf', 'tank': 'Tank', 'ranged': 'Fernkampf', 'artillery': 'Artillerie', 'support': 'Unterstützung', 'mercenary': 'Söldner', 'worker': 'Arbeiter', 'lane': 'Lane-Segment', 'enemy': 'Wellengegner', 'core': 'Saumkern'} def main(): assets = json.loads((PROJECT / 'assets/phase4_manifest.json').read_text())['assets'] cards = [] for faction, label in FACTIONS.items(): selected = [a for a in assets if a['faction'] == faction] for start in range(0, len(selected), 2): pair = selected[start:start + 2] filename = 'reference-' + pair[0]['id'] + '.png' assert (OUT / filename).is_file(), filename title = ' / '.join(a['id'].upper() + ' · ' + a['name'] for a in pair) downloads = ' · '.join( f'{a["id"].upper()}: GLB / ' f'Blender' for a in pair) cards.append(f'
{escape(label)}' f'

{escape(title)}

' f'' f'
') buttons = '' buttons += ''.join(f'' for key, value in FACTIONS.items()) html = ''' Saumwacht · Bild und Modell
Saumwacht / Grafiküberarbeitung / 16. September 2026

Bild und Modell.
Direkt nebeneinander.

Die zwanzig Glutpakt- und Wurzelbund-Figuren wurden mit hochdetaillierter Geometrie neu aufgebaut; Nietenkonvent und Nebelchor behalten ihren bisherigen Detailgrad. Jeder Vergleich zeigt die unveränderte Illustration neben dem aktuellen Modell aus Godot. Klicke auf ein Bild, um es in voller Größe zu öffnen.

Die neuen Glutpakt-Modelle besitzen modellierte Gelenke, Panzerplatten, Metallkanten, Finger und Feuerkammern. Die Illustrationen dienen als Stilvorlage; die Figuren sind eigenständige 3D-Umsetzungen. Der Wurzelbund ergänzt geflochtene Wurzeln, Borkenplatten, Blattadern, Pilzkelche und Heilbecken.

23 Vergleichsbilder · 46 Figuren
''' + '\n'.join(cards) + '''

Die Vorschauen passen die Figuren auf eine gemeinsame Bühne ein; sie vergleichen keine Weltmaßstäbe. Lane, Teamkerne und die zehn Wellengegner wurden ebenfalls überarbeitet. Wellengegner ansehen · Umgebung ansehen · 3D-Werkstatt ansehen

''' (OUT / 'reference-gallery.html').write_text(html, encoding='utf-8') catalog = ['# Saumwacht · Modellkatalog', '', 'Stand: 17. September 2026. Zwanzig Glutpakt- und Wurzelbund-Figuren, zehn Wellengegner, Arbeiter und Kristalle wurden neu aufgebaut; Söldner und Lane erhielten zusätzliche Details. Die 59 Einträge stammen aus `assets/phase4_manifest.json`.', '', 'Maße in Metern: **B = X, H = Y, T = Z** in Godot, auf zwei Nachkommastellen gerundet. ' 'GLBs sind über die IDs verlinkt; native Quellen liegen in `blend/.blend`. ' '[Anleitung](README.md) · [Bildvergleiche](../../docs/screenshots/phase4/reference-gallery.html)', '', '| ID / GLB | Modell | Fraktion | Rolle | Upgrade | Dreiecke | B (m) | H (m) | T (m) |', '| --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: |'] for a in assets: b = a['bounds_godot'] dims = [hi-lo for hi, lo in zip(b['max'], b['min'])] faction = FACTIONS.get(a['faction'], 'Risskreaturen' if a['faction']=='rift' else 'Umgebung') upgrade = 'Ja' if a.get('upgraded', a.get('upgrade', False)) else 'Nein' catalog.append(f'| [{a["id"]}](../../assets/models/{a["file"]}) | {a["name"]} | {faction} | ' f'{ROLES[a["role"]]} | {upgrade} | {a["triangles"]} | ' + ' | '.join(f'{d:.2f}' for d in dims) + ' |') (PROJECT / 'assets-source/phase4/catalog.md').write_text('\n'.join(catalog)+'\n', encoding='utf-8') print('REFERENCE_GALLERY_PASS: 23 comparison sheets, 46 characters; catalog 59 models') if __name__ == '__main__': main()