saumwacht-godot/tools/luneraria/build_asset_matrix.py
Martin Stickelbroeck 4b6a4f530e M0: Bestandsaufnahme, Regelwerk, Architektur und Assetmatrix für Champions of Luneraria
Regelwerk trennt verbindliche Produktregeln, Arbeitsannahmen A-F und
eigene Detailentscheidungen. Assetmatrix wird aus den Manifesten erzeugt.
Baseline-Tests (Legacy) 209/33/463/206/1665 bestanden, Logs unter
docs/luneraria/logs/m0-baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:04:36 +02:00

97 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""Asset reuse matrix for Champions of Luneraria, generated from the runtime manifests.
Run: python3 tools/luneraria/build_asset_matrix.py -> docs/luneraria/ASSETMATRIX.md
Roles marked "vorläufig" are provisional assignments of existing models, not new peoples.
"""
from pathlib import Path
import json
P = Path(__file__).resolve().parents[2]
phase4 = {a['id']: a for a in json.loads((P / 'assets/phase4_manifest.json').read_text())['assets']}
skeletal = {}
for name in ['phase11', 'phase12', 'phase15', 'phase16', 'phase18', 'phase19']:
for a in json.loads((P / f'assets/{name}_manifest.json').read_text())['assets']:
skeletal[a['id']] = (a, name)
# id -> (bisherige Rolle, neue Rolle, Volk/Bund-Zuordnung, Eingriff)
ROLES = {
'g01': ('Glutpakt Nahkampf', 'Truppe Nahkampf', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g02': ('Glutpakt Nahkampf+', 'Truppenaufwertung', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g03': ('Glutpakt Tank', 'Truppe Front', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g04': ('Glutpakt Tank+', 'Truppenaufwertung', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g05': ('Glutpakt Fernkampf', 'Truppe Fernkampf; M2 zusätzlich Held „Funkenlanze“ (vorläufig, mit Heldenmarkierung)', 'Glutkonstrukt / Glutpakt', 'Heldenkennzeichnung in der Darstellung'),
'g06': ('Glutpakt Fernkampf+', 'Truppenaufwertung (Belagerung)', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g07': ('Glutpakt Fläche', 'Truppe Fläche', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g08': ('Glutpakt Fläche+', 'Truppenaufwertung', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g09': ('Glutpakt Unterstützung', 'Truppe Unterstützung', 'Glutkonstrukt / Glutpakt', 'keiner'),
'g10': ('Glutpakt Unterstützung+', 'Truppenaufwertung', 'Glutkonstrukt / Glutpakt', 'keiner'),
'w01': ('Wurzelbund Nahkampf', 'Truppe Nahkampf', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w02': ('Wurzelbund Nahkampf+', 'Truppenaufwertung', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w03': ('Wurzelbund Tank', 'Truppe Front', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w04': ('Wurzelbund Tank+', 'Truppenaufwertung; M2 zusätzlich Held „Ringborke“ (vorläufig)', 'Wurzelwesen / Wurzelbund', 'Heldenkennzeichnung'),
'w05': ('Wurzelbund Fernkampf', 'Truppe Fernkampf', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w06': ('Wurzelbund Fernkampf+', 'Truppenaufwertung', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w07': ('Wurzelbund Fläche', 'Truppe Fläche', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w08': ('Wurzelbund Fläche+', 'Truppenaufwertung', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w09': ('Wurzelbund Heiler', 'Truppe Unterstützung', 'Wurzelwesen / Wurzelbund', 'keiner'),
'w10': ('Wurzelbund Heiler+', 'Truppenaufwertung; M2 zusätzlich Held „Quellgeflecht“ (vorläufig)', 'Wurzelwesen / Wurzelbund', 'Heldenkennzeichnung'),
'e01': ('Wellengegner', 'Monsterwelle (teamgebunden, rotierend)', 'Riftbrut (neutral, pro Team eingefärbt)', 'Teamkennzeichnung'),
'e02': ('Wellengegner', 'Monsterwelle', 'Riftbrut', 'Teamkennzeichnung'),
'e03': ('Wellengegner', 'Monsterwelle', 'Riftbrut', 'Teamkennzeichnung'),
'e04': ('Wellengegner', 'Monsterwelle', 'Riftbrut', 'Teamkennzeichnung'),
'e05': ('Wellengegner', 'Monsterwelle (späte Variante)', 'Riftbrut', 'Teamkennzeichnung'),
'e06': ('Wellengegner', 'Monsterwelle (Unterstützer)', 'Riftbrut', 'Teamkennzeichnung'),
'e07': ('Boss Welle 5', 'Reserve (Wellen-Elite ab höheren Indizes)', 'Riftbrut', 'offen'),
'e08': ('Boss Welle 8', 'Reserve (Wellen-Elite)', 'Riftbrut', 'offen'),
'e09': ('Boss Welle 15', 'Reserve', 'Riftbrut', 'offen'),
'e10': ('Boss Welle 20', 'M2: neutraler Grottenboss und entsandter Boss', 'Riftbrut / Grotte', 'Bossdarstellung, Skalierung'),
's01': ('Söldner', 'Reserve Angriffschampion', 'Söldner', 'offen'),
's02': ('Söldner', 'M2: Angriffschampion Schutz', 'Söldner', 'keiner'),
's03': ('Söldner', 'M2: Angriffschampion Druck auf Unterstützer', 'Söldner', 'keiner'),
's04': ('Söldner', 'M2: Angriffschampion Belagerung', 'Söldner', 'keiner'),
's05': ('Söldner', 'Reserve Angriffschampion', 'Söldner', 'offen'),
'core_team0': ('Saumkern Team 0', 'Ancient Blau (SW)', '—', 'keiner'),
'core_team1': ('Saumkern Team 1', 'Ancient Rot (NO)', '—', 'keiner'),
'worker': ('Arbeiter/Funken', 'nicht im neuen Modus (Arbeitsannahme E); Modell bleibt erhalten', '—', 'keiner'),
'map_lane_segment': ('Lane-Kachel alt', 'nicht verwendet; Lanes neu erzeugt', '—', 'keiner'),
}
for prefix, people in (('m', 'Nietenkonvent (nicht spielbar)'), ('n', 'Nebelchor (nicht spielbar)')):
for i in range(1, 11):
ROLES[f'{prefix}{i:02d}'] = ('nicht spielbar, niedrige Detailstufe', 'Reserve für spätere Völker', people, 'Neuaufbau nötig vor Nutzung')
rows = []
for asset_id in sorted(set(phase4) | set(skeletal)):
base = phase4.get(asset_id, {})
path = base.get('resource_path', '')
source = str(base.get('blend_path', '')).replace(str(P) + '/', '')
tris = base.get('triangles', '')
rig = 'starre Gelenkgruppen'
material = 'HD, starre Teile' if base.get('detail_level') == 'high' else 'niedrige Detailstufe'
if asset_id in skeletal:
record, manifest = skeletal[asset_id]
path = record['resource_path']
source = record.get('source', source)
tris = record['triangles']
rig = '%d Knochen · %d Clips' % (record['bones'], len(record['clips']))
material = 'gewichtetes Skelett (%s)' % manifest
old, new, people, work = ROLES.get(asset_id, ('?', 'offen', '?', 'Prüfung nötig'))
rows.append((asset_id, base.get('name') or base.get('display_name') or skeletal.get(asset_id, ({},))[0].get('name', ''), path, source, rig, tris, old, new, people, material, work))
extra = [
('menu-stage', 'Menü-Diorama', 'res://assets/environment/menu-stage.glb', 'assets-source/phase20/build_menu_stage.py', 'statisch', '48660', 'Hauptmenü Phase 20A', 'Hauptmenü (Titel angepasst)', '—', 'Kluft-Shader', 'keiner'),
('verdant-rift', 'Grüne Kluft (2-Bretter-Arena)', 'res://assets/environment/verdant-rift.glb', 'assets-source/phase10/verdant-rift.blend', 'statisch', '—', 'Spielfeld Saumwacht', 'Legacy-Modus; Materialien/Shader für die neue Karte wiederverwendet', '—', 'triplanare Kluft-Shader', 'keine neue Nutzung als Karte'),
('compare-rock / compare-tree', 'Fels, Baum', 'res://assets/compare/', 'assets-source/phase14/build_compare_props.py', 'statisch', '360 / 852', 'Vergleichsszene', 'Kartendekoration Dschungel', '—', 'einfach', 'keiner'),
('Türme', '—', '(fehlt)', '—', '—', '—', '—', '16 Lanetürme/Basistürme', '—', '—', 'neu: Blender-Generator (M1)'),
('Bossgrotte', '—', '(fehlt)', '—', '—', '—', '—', 'Grottenbau nahe Zentrum', '—', '—', 'neu (M2)'),
('Emblem/Theme', 'Saumwacht-Theme', 'scripts/ui/theme/, assets/ui/', 'Code / SVG', '—', '—', 'Menü 20A', 'Theme weiterverwendet, Titel/Emblem Luneraria', '—', 'Vektor', 'Titelschrift/Emblem ergänzen'),
]
lines = ['# Asset-Wiederverwendungsmatrix', '',
'Erzeugt mit `python3 tools/luneraria/build_asset_matrix.py` aus den Laufzeitmanifesten. „Volk/Bund“ ist die Datenzuordnung. Die sieben Zielvölker (Menschen, Elfen, Zwerge, Trolle, Feen, Orks, Biestmenschen) haben noch **keine eigenen Modelle**; bestehende Wesen bleiben als eigene Bünde Teil der Welt. Als „vorläufig“ markierte Rollen sind Zuordnungen vorhandener Modelle, keine neu erstellten Völker.', '',
'| ID | Name | Pfad | Quelle | Rig / Clips | Dreiecke | bisherige Rolle | neue Rolle | Volk / Bund | Materialzustand | Eingriff |',
'| --- | --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- |']
for row in rows + extra:
lines.append('| ' + ' | '.join(str(x) for x in row) + ' |')
(P / 'docs/luneraria').mkdir(parents=True, exist_ok=True)
(P / 'docs/luneraria/ASSETMATRIX.md').write_text('\n'.join(lines) + '\n')
print('ASSET_MATRIX', len(rows) + len(extra), 'rows')