Files
settled-reach/tooling/generate-character-manifest
T
jpmschweitzerandClaude Opus 4.6 bc168c861a feat(client): hair highlight swatch + manifest JSON for exports (#719, #720)
#719: Add display-only hair highlight swatch (Option B) — auto-derived
from primary tint, non-interactive (MOUSE_FILTER_IGNORE).

#720: Remove DirAccess.open() fallback scanning (breaks in PCK exports).
Fully populate manifest.json (11 body types, 14 hair, 4 heads, 4
eyebrows, 8 clothing). Add tooling/generate-character-manifest script
and make manifest target for regeneration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 23:34:23 +02:00

112 lines
3.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate client/assets/characters/manifest.json from the asset directories.
Run this whenever artists add new assets so the manifest stays in sync:
tooling/generate-character-manifest
The manifest is the single source of truth for CharacterCreation asset IDs.
DirAccess.open() cannot enumerate res:// paths in exported PCK builds (#720).
Output: client/assets/characters/manifest.json
"""
import json
import os
import sys
ASSETS_ROOT = os.path.join(os.path.dirname(__file__), "..", "client", "assets", "characters")
MANIFEST_PATH = os.path.join(ASSETS_ROOT, "manifest.json")
def scan_glb_ids(dir_path: str) -> list[str]:
"""Return sorted list of .glb basenames (without extension) in dir_path."""
if not os.path.isdir(dir_path):
return []
ids = sorted(
os.path.splitext(f)[0]
for f in os.listdir(dir_path)
if f.endswith(".glb") and not f.startswith(".")
)
return ids
def scan_subdirs(dir_path: str) -> list[str]:
"""Return sorted list of subdirectory names in dir_path."""
if not os.path.isdir(dir_path):
return []
return sorted(
d for d in os.listdir(dir_path)
if os.path.isdir(os.path.join(dir_path, d)) and not d.startswith(".")
)
def infer_clothing_slot(item_id: str) -> str:
"""Infer clothing slot from item_id by convention."""
id_lower = item_id.lower()
if any(k in id_lower for k in ("boot", "shoe", "sandal", "slipper")):
return "feet"
if any(k in id_lower for k in ("pant", "trouser", "skirt", "short")):
return "legs"
if any(k in id_lower for k in ("glove", "gauntlet")):
return "hands"
# Default: torso (jacket, tunic, shirt, coveralls, vest, etc.)
return "torso"
def build_manifest() -> dict:
# Body types: subdirs under bodies/
body_types = scan_subdirs(os.path.join(ASSETS_ROOT, "bodies"))
# Heads: .glb files in heads/templates/
heads = scan_glb_ids(os.path.join(ASSETS_ROOT, "heads", "templates"))
# Hair: .glb files in hair/
hair = scan_glb_ids(os.path.join(ASSETS_ROOT, "hair"))
# Facial hair: .glb files in facial_hair/
facial_hair = scan_glb_ids(os.path.join(ASSETS_ROOT, "facial_hair"))
# Eyebrows: .glb files in eyebrows/
eyebrows = scan_glb_ids(os.path.join(ASSETS_ROOT, "eyebrows"))
# Clothing: subdirs under clothing/, each assigned a slot
clothing_items = scan_subdirs(os.path.join(ASSETS_ROOT, "clothing"))
clothing: dict = {}
for item_id in clothing_items:
clothing[item_id] = {"slot": infer_clothing_slot(item_id)}
# Accessories: no directory yet — leave empty
accessories: list = []
acc_dir = os.path.join(ASSETS_ROOT, "accessories")
if os.path.isdir(acc_dir):
accessories = scan_glb_ids(acc_dir)
return {
"body_types": body_types,
"heads": heads,
"hair": hair,
"facial_hair": facial_hair,
"eyebrows": eyebrows,
"clothing": clothing,
"accessories": accessories,
}
def main() -> None:
manifest = build_manifest()
output = json.dumps(manifest, indent=2) + "\n"
with open(MANIFEST_PATH, "w", encoding="utf-8") as f:
f.write(output)
print(f"Written: {MANIFEST_PATH}")
print(f" body_types : {len(manifest['body_types'])}")
print(f" heads : {len(manifest['heads'])}")
print(f" hair : {len(manifest['hair'])}")
print(f" facial_hair: {len(manifest['facial_hair'])}")
print(f" eyebrows : {len(manifest['eyebrows'])}")
print(f" clothing : {len(manifest['clothing'])}")
print(f" accessories: {len(manifest['accessories'])}")
if __name__ == "__main__":
main()