Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
6.4 KiB
Python
132 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Bundle the modular bpy sources into nine genuinely self-contained scripts.
|
|
|
|
No Blender dependency for this build step. Source comments and shape functions
|
|
are preserved; only imports of our own modules and the batch dispatcher vanish.
|
|
"""
|
|
import ast
|
|
from pathlib import Path
|
|
from pprint import pformat
|
|
import sys
|
|
|
|
PROJECT = Path(__file__).resolve().parents[1]
|
|
SOURCE = PROJECT / 'assets-source' / 'phase4'
|
|
MODULES = SOURCE / 'blender'
|
|
OUTPUT = SOURCE / 'standalone'
|
|
sys.path.insert(0, str(MODULES))
|
|
from catalog import specs
|
|
|
|
ENTRIES = [
|
|
('melee', 'frontline', 'g01', 'Nahkämpfer'),
|
|
('tank', 'frontline', 'g03', 'Tank'),
|
|
('ranged', 'backline', 'g05', 'Fernkämpfer'),
|
|
('artillery', 'backline', 'g07', 'Flächenkämpfer'),
|
|
('support', 'backline', 'g09', 'Unterstützer'),
|
|
('mercenary', 'environment', 's01', 'Söldner'),
|
|
('worker', 'environment', 'worker', 'Funkenleser'),
|
|
('lane', 'environment', 'map_lane_segment', 'Lane-Segment'),
|
|
('core', 'environment', 'core_team0', 'Teamkerne'),
|
|
]
|
|
|
|
|
|
def bundled_source(filename, skip_functions=()):
|
|
source = (MODULES / filename).read_text(encoding='utf-8')
|
|
lines = source.splitlines(keepends=True)
|
|
skipped = set()
|
|
for node in ast.parse(source).body:
|
|
own_import = isinstance(node, ast.ImportFrom) and (node.module in ('common', 'catalog') or node.module.startswith('reference_'))
|
|
omitted_function = isinstance(node, ast.FunctionDef) and node.name in skip_functions
|
|
docstring = isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str)
|
|
if own_import or omitted_function or docstring:
|
|
skipped.update(range(node.lineno - 1, node.end_lineno))
|
|
return ''.join(line for i, line in enumerate(lines) if i not in skipped).strip()
|
|
|
|
|
|
CONFIG = '''
|
|
from pathlib import Path
|
|
import sys
|
|
import bpy
|
|
|
|
# === ANPASSBARE PARAMETER ================================================
|
|
ASSET_ID = {asset_id!r} # Verfügbare IDs stehen unmittelbar unter diesem Block.
|
|
EXPORT_DIR = None # Optional: r"C:/Spiel/assets/models" oder "/home/.../models".
|
|
BLEND_DIR = None # Optionaler Ordner für die bearbeitbare .blend-Datei.
|
|
HEIGHT = None # None = Katalogmaß; sonst Zielhöhe in Metern, z. B. 1.5.
|
|
PALETTE_OVERRIDES = {{}} # Beispiel: {{'accent': 'D9A441'}}; RGB-Hex ohne Alpha.
|
|
KEEP_SCENE = True # Generiertes Modell nach Ausführung in Blender anzeigen.
|
|
# Formteile und Proportionen sind weiter unten als kleine Funktionen editierbar.
|
|
# Bei großen Waffen begrenzt der Zell-Footprint ggf. die endgültige Höhe.
|
|
# Dieses Skript braucht nur Blender/bpy, keine Projektmodule oder JSON-Dateien.
|
|
|
|
def _script_directory():
|
|
candidate = globals().get('__file__', '')
|
|
if candidate and Path(candidate).is_file():
|
|
return Path(candidate).resolve().parent
|
|
text = getattr(getattr(bpy.context, 'space_data', None), 'text', None)
|
|
if text is not None and text.filepath:
|
|
return Path(bpy.path.abspath(text.filepath)).resolve().parent
|
|
return Path.cwd()
|
|
|
|
SCRIPT_DIR = _script_directory()
|
|
PROJECT = next((p for p in [SCRIPT_DIR, *SCRIPT_DIR.parents]
|
|
if (p / 'project.godot').is_file()), SCRIPT_DIR)
|
|
IN_PROJECT = (PROJECT / 'project.godot').is_file()
|
|
SOURCE = PROJECT / 'assets-source' / 'phase4' if IN_PROJECT else SCRIPT_DIR / 'saumwacht-source'
|
|
DEFAULT_EXPORT_DIR = PROJECT / 'assets' / 'models' if IN_PROJECT else SCRIPT_DIR / 'saumwacht-export'
|
|
DEFAULT_BLEND_DIR = SOURCE / 'blend'
|
|
'''
|
|
|
|
MAIN = '''
|
|
|
|
def main():
|
|
# CLI-Optionen sind optional; im Blender-Scripting-Tab genügt "Run Script".
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--id', default=ASSET_ID)
|
|
parser.add_argument('--export-dir', default=EXPORT_DIR)
|
|
parser.add_argument('--blend-dir', default=BLEND_DIR)
|
|
parser.add_argument('--height', type=float, default=HEIGHT)
|
|
parser.add_argument('--no-keep-scene', action='store_true')
|
|
args = parser.parse_args(sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else [])
|
|
build_one(args.id, export_dir=args.export_dir, blend_dir=args.blend_dir,
|
|
height=args.height, palette_overrides=PALETTE_OVERRIDES,
|
|
keep_scene=KEEP_SCENE and not args.no_keep_scene)
|
|
print('STANDALONE_PASS:', args.id)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
'''
|
|
|
|
|
|
def main():
|
|
OUTPUT.mkdir(parents=True, exist_ok=True)
|
|
for role, module, asset_id, label in ENTRIES:
|
|
selected = [s for s in specs() if s['role'] == role]
|
|
header = f'"""Saumwacht · {label} · eigenständiges Blender-Python-Skript.\n\n'
|
|
header += 'In Blender: Scripting > Text > Open > diese Datei > Run Script.\n'
|
|
header += 'Vorhandene Szenen bleiben erhalten. GLB und .blend werden automatisch exportiert.\n'
|
|
header += 'Generiert aus den modularen Quellen mit tools/build_standalone_model_scripts.py.\n'
|
|
header += 'Getestet mit Blender 5.2.1; verwendet grundlegende bpy/glTF-APIs.\n"""\n'
|
|
data = '\n# === VERFÜGBARE VARIANTEN ===============================================\n'
|
|
data += 'ASSET_SPECS = ' + pformat(selected, width=105, sort_dicts=False) + '\n'
|
|
data += '\ndef get_spec(asset_id):\n for spec in ASSET_SPECS:\n if spec["id"] == asset_id:\n return dict(spec)\n raise ValueError("Unbekannte ID für dieses Skript: " + asset_id)\n'
|
|
dispatcher = '\ndef _builder(role):\n return globals()["build_" + role]\n'
|
|
body = '\n\n'.join([
|
|
bundled_source('common.py'), bundled_source('reference_geometry.py'),
|
|
bundled_source('reference_high_detail.py'), bundled_source('reference_living_detail.py'), bundled_source('reference_sculpt_detail.py'),
|
|
bundled_source('reference_glutpakt.py'), bundled_source('reference_nebelchor.py'),
|
|
bundled_source('reference_nietenkonvent.py'), bundled_source('reference_wurzelbund.py'),
|
|
bundled_source('reference_neutral.py'),
|
|
bundled_source(module + '.py'), dispatcher,
|
|
bundled_source('pipeline.py', ('_builder', 'write_manifest')),
|
|
])
|
|
content = header + CONFIG.format(asset_id=asset_id) + data + '\n' + body + MAIN
|
|
path = OUTPUT / ('create_' + role + '.py')
|
|
compile(content, str(path), 'exec')
|
|
path.write_text(content, encoding='utf-8')
|
|
print(path.name, '·', len(selected), 'Varianten')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|