Files
aetherbound-guild/tools/sync_p9_0r2_animation_assets.py

160 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Build the P9.0R2 runtime animation tree from accepted source frames."""
from __future__ import annotations
import hashlib
import json
import shutil
import sys
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "runtime/assets/animations"
GEN = ROOT / "docs/prototype/generation/phase9_0r2_character_animation_01/runs"
PHASE6 = ROOT / "docs/prototype/generation/phase6_motion_proof_03/runs"
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def copy_frame(source: Path, target: Path, records: list[dict[str, object]]) -> None:
if not source.is_file():
raise SystemExit(f"missing animation source: {source}")
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
records.append(
{
"source": source.relative_to(ROOT).as_posix(),
"runtime": target.relative_to(ROOT).as_posix(),
"sha256": sha256(target),
"width": Image.open(target).width,
"height": Image.open(target).height,
}
)
def copy_cycle(
character: str,
state: str,
source_root: Path,
frame_names: list[str],
records: list[dict[str, object]],
) -> None:
for index, name in enumerate(frame_names):
copy_frame(
source_root / name,
OUT / character / state / f"frame-{index}.png",
records,
)
def main() -> int:
records: list[dict[str, object]] = []
hearthguard = PHASE6 / "recruit-aegis/accepted"
for state in ("idle", "advance", "guard", "basic_attack", "victory"):
manifest_path = hearthguard / state / "cycle-manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
copy_cycle(
"hearthguard",
state,
hearthguard / state,
[str(frame["output"]) for frame in manifest["frames"]],
records,
)
riftblade = PHASE6 / "enemy-riftblade/accepted"
for state in ("idle", "advance", "telegraph", "attack", "hit", "death"):
manifest_path = riftblade / state / "cycle-manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
copy_cycle(
"riftblade",
state,
riftblade / state,
[str(frame["output"]) for frame in manifest["frames"]],
records,
)
custom = {
"wayrunner": {
"ready": GEN / "wayrunner-r2/curated/ready.png",
"advance_1": GEN / "wayrunner-r2/curated/advance_1.png",
"advance_2": GEN / "wayrunner-r2/curated/advance_2.png",
"evade": GEN / "wayrunner-r2/curated/evade.png",
"counter": GEN / "wayrunner-r2/curated/counter.png",
"recover": GEN / "wayrunner-r2/curated/recover.png",
},
"farstring": {
"ready": GEN / "farstring/curated/ready.png",
"advance_1": GEN / "farstring/curated/advance_1.png",
"raise": GEN / "farstring/curated/raise.png",
"draw": GEN / "farstring/curated/draw.png",
"release": GEN / "farstring/curated/release.png",
},
"kindhand": {
"ready": GEN / "kindhand/curated/ready.png",
"advance_1": GEN / "kindhand/curated/advance_1.png",
"advance_2": GEN / "kindhand/curated/advance_2.png",
"raise": GEN / "kindhand/curated/raise.png",
"heal": GEN / "kindhand/curated/heal.png",
"recover": GEN / "kindhand/curated/recover.png",
},
}
runtime_sequences = {
"wayrunner": {
"idle": ["ready", "recover"],
"advance": ["advance_1", "advance_2"],
"evade_counter": ["ready", "evade", "counter", "recover"],
},
"farstring": {
"idle": ["ready", "raise"],
"advance": ["ready", "advance_1"],
"draw_release": ["ready", "raise", "draw", "release"],
},
"kindhand": {
"idle": ["ready", "recover"],
"advance": ["advance_1", "advance_2"],
"lantern_heal": ["ready", "raise", "heal", "recover"],
},
}
for character, states in runtime_sequences.items():
for state, names in states.items():
for index, name in enumerate(names):
copy_frame(
custom[character][name],
OUT / character / state / f"frame-{index}.png",
records,
)
manifest = {
"kind": "aetherbound-p9-0r2-runtime-animation-assets",
"generated_calls": 4,
"generated_cost_usd": 0.056,
"rejected": [
{
"run": "wayrunner",
"reason": "detached duplicate blade fragment and missing held dagger at counter impact",
}
],
"records": records,
}
(OUT / "asset-manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(f"ABG_P9_0R2_ANIMATION_ASSETS_OK files={len(records)} calls=4 cost_usd=0.056")
return 0
if __name__ == "__main__":
sys.exit(main())