saumwacht-godot/tools/luneraria/build_asset_matrix.py
Martin Stickelbroeck faf0fbdef0 M3: Balance, Kartenprops, sieben Volksrepräsentanten und Abnahme
Bots planen Items/Champions vor der Armee und helfen deterministisch,
damit Spiegelmatches fair bleiben. Boss und Profile neu abgestimmt;
Balance-, Stress- und Volks-Werkzeuge sowie Kartenprops ergänzt.
2026-09-28 00:41:05 +02:00

116 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)
peoples = {a['id']: a for a in json.loads((P / 'assets/luneraria/peoples_manifest.json').read_text())['assets']}
# 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; zusätzlich Held „Funkenlanze“ (vorläufig; Goldring, Namensschild, ×1,25)', 'Glutkonstrukt / Glutpakt', 'umgesetzt (M2)'),
'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; zusätzlich Held „Ringborke“ (vorläufig)', 'Wurzelwesen / Wurzelbund', 'umgesetzt (M2)'),
'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; zusätzlich Held „Quellgeflecht“ (vorläufig)', 'Wurzelwesen / Wurzelbund', 'umgesetzt (M2)'),
'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', 'neutraler Grottenboss und entsandter Boss (×1,7)', 'Riftbrut / Grotte', 'umgesetzt (M2)'),
's01': ('Söldner', 'Reserve Angriffschampion', 'Söldner', 'offen'),
's02': ('Söldner', 'Angriffschampion Schutz (Rostschieber)', 'Söldner', 'umgesetzt (M2)'),
's03': ('Söldner', 'Angriffschampion Druck auf Unterstützer (Dunstsänger)', 'Söldner', 'umgesetzt (M2)'),
's04': ('Söldner', 'Angriffschampion Belagerung (Keilbrecher)', 'Söldner', 'umgesetzt (M2)'),
'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'),
}
PEOPLE_ROLES = {
'p_mensch': ('—', 'Volksrepräsentant Front (Ritter)', 'Menschen', 'neu (M3)'),
'p_elf': ('—', 'Volksrepräsentant Fernkampf (Waldläuferin)', 'Elfen', 'neu (M3)'),
'p_zwerg': ('—', 'Volksrepräsentant Front (Schildträger)', 'Zwerge', 'neu (M3)'),
'p_troll': ('—', 'Volksrepräsentant Fläche (Steinhauer)', 'Trolle', 'neu (M3)'),
'p_fee': ('—', 'Volksrepräsentant Unterstützung (Lichtheilerin)', 'Feen', 'neu (M3)'),
'p_ork': ('—', 'Volksrepräsentant Nahkampf (Kriegshäuptling)', 'Orks', 'neu (M3)'),
'p_biest': ('—', 'Volksrepräsentant Nahkampf (Klauenjäger)', 'Biestmenschen', 'neu (M3)'),
}
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))
for asset_id in sorted(peoples):
record = peoples[asset_id]
old, new, people, work = PEOPLE_ROLES[asset_id]
rows.append((asset_id, record.get('name', ''), record['resource_path'], record.get('source', ''),
'%d Knochen · %d Clips' % (record['bones'], len(record['clips'])), record['triangles'],
old, new, people, 'gewichtetes Skelett (peoples, LP_*-Materialien)', 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'),
('lane_tower', 'Laneturm', 'res://assets/luneraria/tower.glb', 'assets-source/luneraria/build_tower.py', 'statisch', '648', '—', '16 Lanetürme und Basistürme, Kristall zur Laufzeit in Teamfarbe', '—', 'ENV_*-Kluftshader, Kristall emissiv', 'neu (M1)'),
('boss_grotto', 'Bossgrotte', 'res://assets/luneraria/grotto.glb', 'assets-source/luneraria/build_grotto.py', 'statisch', '1888', '—', 'Felsinsel mit Höhle auf der Symmetrieachse, zwei gespiegelte Zugänge', '—', 'ENV_*-Kluftshader, violette Kristalle', 'neu (M2)'),
('map_props', 'Kartenprops (Bäume, Felsen, Büsche, Brücken, Bordsteine, Klippen, Banner)', 'res://assets/luneraria/map_props.glb', 'assets-source/luneraria/build_map_props.py', 'statisch', 'MultiMesh', '—', '3D-Kartendekoration der drei Lanes, Brücken über die Kluft, Klampterrahmen', '—', 'ENV_*-Kluftshader', 'neu (M3)'),
('luneraria-emblem', 'Emblem Luneraria', 'res://assets/ui/emblems/luneraria.svg', 'SVG', '—', '—', '—', 'Titel und Menü', '—', 'Vektor', 'neu (M1)'),
('Emblem/Theme', 'Saumwacht-Theme', 'scripts/ui/theme/, assets/ui/', 'Code / SVG', '—', '—', 'Menü 20A', 'Theme weiterverwendet, Titel/Emblem Luneraria', '—', 'Vektor', 'Titelschrift/Emblem ergänzen'),
('compare-rock / compare-tree', 'Fels, Baum (Alt)', 'res://assets/compare/', 'assets-source/phase14/build_compare_props.py', 'statisch', '360 / 852', 'Vergleichsszene', 'ab M3 durch map_props ersetzt', '—', 'einfach', 'abgelöst (M3)'),
]
lines = ['# Asset-Wiederverwendungsmatrix', '',
'Erzeugt mit `python3 tools/luneraria/build_asset_matrix.py` aus den Laufzeitmanifesten. „Volk/Bund“ ist die Datenzuordnung. Die sieben Zielvölker haben ab M3 **eigene Repräsentantenmodelle** (`p_*`); sie sind noch nicht als spielbare Fraktionen eingebunden. 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')