squad-proto/tools/asset_coverage.py
z.kirill 8d35f4c2ec Конвейер ассетов: манифесты, риги, каталоги, инструменты
Всё, из чего собираются спрайты, и ничего собранного. Источник истины —
assets/manifests и assets/*/catalog.yaml; PNG, .sfa и contact sheet остаются
за бортом, потому что воспроизводятся командой (docs/12-assets.md).

  assets/manifests   что и как рендерить: окружение, твари, агенты, предметы
  assets/agents      паперdoll-риг восьми направлений, каталог снаряжения
  assets/monsters    пять морфологий, каждая своим ригом (docs/14-monsters.md)
  assets/sources     исходные развёртки и импорты
  assets/art_masters мастер-развёртки, по которым сверяется стиль
  tools/             сборка ассетов, покрытие, генераторы, blender-скрипты
  generated/         заголовки с константами SF_ASSET_*: без них игра не
                     соберётся, поэтому они в репозитории, хотя и производные

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:06:09 +03:00

73 lines
2.6 KiB
Python

"""Text-only coverage gate for every visual kind currently present in the game."""
from __future__ import annotations
import argparse
from collections import defaultdict
from pathlib import Path
import sys
import yaml
ROOT = Path(__file__).resolve().parents[1]
COVERAGE = ROOT / "assets/content_coverage.yaml"
MANIFESTS = ROOT / "assets/manifests"
def declared_assets() -> set[str]:
result: set[str] = set()
for path in sorted(MANIFESTS.glob("*.yaml")):
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
result.update(key for key, value in raw.items()
if key not in {"version", "defaults"} and isinstance(value, dict))
return result
def requirements(spec: dict) -> dict[str, list[str]]:
result: dict[str, list[str]] = defaultdict(list)
agent = spec["agents"]
for slot, components in agent["components"].items():
for component in components:
for animation in agent["animations"]:
result[f"agent/{slot}"].append(f"agent_{component}_{animation}")
for kind in spec["enemies"]["kinds"]:
for animation in spec["enemies"]["animations"]:
result["enemy"].append(f"{kind}_{animation}")
result["environment"].extend(spec["environment"]["required"])
result["item_icon"].extend(spec["item_icons"]["required"])
return result
def report(release: bool) -> int:
spec = yaml.safe_load(COVERAGE.read_text(encoding="utf-8"))
if spec.get("version") != 1:
raise ValueError("assets/content_coverage.yaml: unsupported version")
have = declared_assets()
groups = requirements(spec)
missing_total = 0
print(f"{'GROUP':<20} {'HAVE':>5} {'NEED':>5} {'MISSING':>7}")
print("-" * 40)
for group, wanted in groups.items():
missing = sorted(set(wanted) - have)
missing_total += len(missing)
print(f"{group:<20} {len(wanted)-len(missing):>5} {len(wanted):>5} {len(missing):>7}")
for asset in missing:
print(f" - {asset}")
covered = sum(len(v) for v in groups.values()) - missing_total
total = sum(len(v) for v in groups.values())
print(f"coverage: {covered}/{total}; missing={missing_total}")
return 1 if release and missing_total else 0
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--release", action="store_true", help="fail if any required asset is absent")
return report(parser.parse_args().release)
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, KeyError) as error:
print(f"ERROR: {error}", file=sys.stderr)
raise SystemExit(2)