fix: honor non-loop battle animations
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
@@ -50,6 +51,28 @@ def frame_files(state_dir: Path) -> list[Path]:
|
||||
return files
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def animation_contract(state_dir: Path) -> tuple[str, bool]:
|
||||
manifest_path = state_dir.parent / "cycle-manifest.json"
|
||||
if not manifest_path.is_file():
|
||||
raise SystemExit(f"missing cycle manifest: {manifest_path}")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
frames = frame_files(state_dir)
|
||||
if len(manifest.get("frames", [])) != len(frames):
|
||||
raise SystemExit(f"cycle manifest frame mismatch: {manifest_path}")
|
||||
return str(manifest["state"]), bool(manifest["loop"])
|
||||
|
||||
|
||||
def selected_frame(actor: Actor, tick: int) -> tuple[Path, str, bool, int]:
|
||||
frames = frame_files(actor.state_dir)
|
||||
state, loop = animation_contract(actor.state_dir)
|
||||
if loop:
|
||||
frame_index = (tick + actor.phase) % len(frames)
|
||||
else:
|
||||
frame_index = min(max(tick - actor.phase, 0), len(frames) - 1)
|
||||
return frames[frame_index], state, loop, frame_index
|
||||
|
||||
|
||||
def fitted_sprite(path: Path, target_height: int) -> tuple[Image.Image, tuple[int, int, int, int]]:
|
||||
source = Image.open(path).convert("RGBA")
|
||||
alpha = source.getchannel("A")
|
||||
@@ -64,8 +87,7 @@ def fitted_sprite(path: Path, target_height: int) -> tuple[Image.Image, tuple[in
|
||||
|
||||
|
||||
def paste_actor(canvas: Image.Image, actor: Actor, tick: int) -> dict[str, object]:
|
||||
frames = frame_files(actor.state_dir)
|
||||
source = frames[(tick + actor.phase) % len(frames)]
|
||||
source, state, loop, frame_index = selected_frame(actor, tick)
|
||||
sprite, source_bbox = fitted_sprite(source, actor.height)
|
||||
left = round(actor.x - sprite.width / 2)
|
||||
top = actor.bottom - sprite.height
|
||||
@@ -76,6 +98,10 @@ def paste_actor(canvas: Image.Image, actor: Actor, tick: int) -> dict[str, objec
|
||||
canvas.alpha_composite(sprite, (left, top))
|
||||
return {
|
||||
"side": actor.side,
|
||||
"state": state,
|
||||
"loop": loop,
|
||||
"animation_tick": tick,
|
||||
"frame_index": frame_index,
|
||||
"source": str(source),
|
||||
"source_sha256": sha256(source),
|
||||
"source_alpha_bbox": list(source_bbox),
|
||||
@@ -200,7 +226,24 @@ def main() -> None:
|
||||
canvas = background.convert("RGBA")
|
||||
actor_reports: list[dict[str, object]] = []
|
||||
for actor in sorted(enemies, key=lambda item: item.bottom):
|
||||
actor_reports.append(paste_actor(canvas, actor, tick))
|
||||
if actor.high_salience:
|
||||
state = "telegraph" if tick < 6 else "attack"
|
||||
state_dir = repo / (
|
||||
"docs/prototype/generation/phase6_motion_proof_03/runs/"
|
||||
f"enemy-riftblade/accepted/{state}/frames"
|
||||
)
|
||||
actor = Actor(
|
||||
actor.side,
|
||||
state_dir,
|
||||
actor.x,
|
||||
actor.bottom,
|
||||
actor.height,
|
||||
0,
|
||||
actor.high_salience,
|
||||
)
|
||||
actor_reports.append(paste_actor(canvas, actor, tick if tick < 6 else tick - 6))
|
||||
else:
|
||||
actor_reports.append(paste_actor(canvas, actor, tick))
|
||||
boss_state = "telegraph" if tick < 8 else "attack"
|
||||
boss_dir = repo / f"docs/prototype/generation/phase6_motion_proof_03/runs/boss-tempest-seraph/accepted/{boss_state}/frames"
|
||||
boss_actor = Actor("boss", boss_dir, boss_template.x, boss_template.bottom, boss_template.height, 0, True)
|
||||
|
||||
Reference in New Issue
Block a user