160 lines
5.3 KiB
Python
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())
|