art: prove crowded battle motion scale
@@ -0,0 +1,61 @@
|
||||
# Phase 6 Battle-Scale Motion Proof 04
|
||||
|
||||
Date: 2026-08-12
|
||||
Stage: `BATTLE_SCALE_CANDIDATE_READY`
|
||||
Motion authority: `../phase6_motion_proof_03/`
|
||||
Style authority: `../phase6_ui_direction_01/outputs/05_battle_ui_japanese_scale.png`
|
||||
|
||||
## Purpose
|
||||
|
||||
Test the accepted transparent motion assets in the actual landscape battle
|
||||
density before generating more Professions, enemies or bosses. This is a
|
||||
non-runtime production proof, not gameplay implementation.
|
||||
|
||||
## Three-Second Read
|
||||
|
||||
1. A ten-member Company is moving as one ordered horizontal force.
|
||||
2. A materially busy enemy group is contesting the same ground.
|
||||
3. One large Tempest Seraph boss remains distinct behind ordinary enemies.
|
||||
|
||||
## Layer And Scale Contract
|
||||
|
||||
- Canvas: opaque `2048x1152` landscape.
|
||||
- Background: one empty soft-painted aether-ruin battlefield generated through
|
||||
API Mart GPT-Image-2. No people, creatures, UI, text, VFX or embedded action.
|
||||
- Allies: exactly ten accepted Recruit sprites, right-facing, each `130-155px`
|
||||
alpha-content height (`11.3-13.5%` of canvas height).
|
||||
- Ordinary enemies: at least twelve accepted Riftblade sprites, left-facing,
|
||||
each `120-145px` alpha-content height, with controlled depth overlap.
|
||||
- Boss: one accepted Tempest Seraph, left-facing, `280-315px` alpha-content
|
||||
height.
|
||||
- UI proof layer: compact top force meters and bottom ten-slot rail only. It is
|
||||
deterministic placeholder chrome and contains no generated copy.
|
||||
- VFX: none. Body motion, scale, overlap and silhouette must pass before
|
||||
detached telegraph/impact effects are introduced.
|
||||
|
||||
The background, actor sources, UI proof layer and future VFX remain separate.
|
||||
No generated full-screen screenshot is sliced into actor sprites.
|
||||
|
||||
## Motion Proof
|
||||
|
||||
Produce 16 frames at 8 FPS. Allies and ordinary enemies use phase-offset
|
||||
accepted cycles. The boss performs its accepted telegraph followed by attack.
|
||||
Output one full-size opening frame, one five-state contact sheet and one H.264
|
||||
MP4. Deterministic audit records source hashes, actor counts, content heights,
|
||||
canvas boundaries and output hashes.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ten allies, at least twelve ordinary enemies and one boss are present in
|
||||
every frame;
|
||||
- ordinary actors stay inside their required play-height ranges and the boss
|
||||
remains inside its required range;
|
||||
- no actor alpha content is cropped by the canvas or HUD safe bands;
|
||||
- ally/enemy facing and collision direction read without labels;
|
||||
- front silhouettes remain separable under overlap at `1280x720` review size;
|
||||
- animation has no identity pop, anchor jump or state-reset seam introduced by
|
||||
composition;
|
||||
- background does not contain baked people, creatures, UI, text or VFX.
|
||||
|
||||
This proof can reach `processed` and `agent_visual_reviewed` only. Human visual,
|
||||
engine runtime, touch, audio, device and release gates remain open.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Phase 6 Battle-Scale Motion Proof 04 Visual Review
|
||||
|
||||
Date: 2026-08-12
|
||||
Evidence level: `processed` and `agent_visual_reviewed`
|
||||
Human state: `OPEN`
|
||||
|
||||
## Result
|
||||
|
||||
The bounded battle-scale pipeline is viable. The opening frame and 2-second
|
||||
motion proof show exactly ten allied actors, twelve ordinary enemies and one
|
||||
boss on the approved landscape camera. Ordinary actors occupy `130-155px` or
|
||||
`120-145px` of the `1152px` canvas; the boss occupies `302px`. The Company,
|
||||
enemy group and boss remain separable after downscaling to `1280x720`.
|
||||
|
||||
## Focal Review
|
||||
|
||||
| Field | Observation |
|
||||
|---|---|
|
||||
| Intended read | ordered Company, contested ground, large boss |
|
||||
| Actual focal order | horizontal collision line, pale large boss, force meters and roster rail |
|
||||
| Identity | Japanese 2D thin-line actors over a soft painted daylight aether ruin |
|
||||
| State feedback | allies and enemies animate accepted phase-offset states; boss telegraphs then attacks |
|
||||
| Play-size issues | actor scale passes; repeated Aegis identity proves density but not Profession variety |
|
||||
| Evidence | processed composition, deterministic audit, agent visual review |
|
||||
| Verdict | `PASS` for scale/motion pipeline; content diversity remains open |
|
||||
|
||||
The background task `task_01KZSD6PE31P8512H61T0Q1Z0V` cost USD 0.014. It
|
||||
contains no visible person, creature, interface, copy, attack or detached VFX.
|
||||
Its open ground lane keeps the actor layer editable and readable.
|
||||
|
||||
## Verification
|
||||
|
||||
- `16` opaque `2048x1152` frames at `8 FPS`, plus opening PNG, five-frame
|
||||
contact sheet and H.264 MP4.
|
||||
- Audit proves `10` allies, `12` ordinary enemies and `1` boss in every frame,
|
||||
contracted content heights and no actor outside the battle safe region.
|
||||
- A fresh independent output directory reproduced the audit, opening, contact
|
||||
sheet and all 16 frame PNGs byte-for-byte.
|
||||
- MP4 probe reports `2048x1152`, `8/1 FPS`, `16` frames and `2.0s` duration.
|
||||
- No detached VFX are present, so body motion and silhouette readability are
|
||||
not being rescued by particles or screen effects.
|
||||
|
||||
## Honest Limits
|
||||
|
||||
This proof intentionally repeats the one accepted Aegis Recruit and one
|
||||
accepted Riftblade enemy. It validates scale, facing, density, overlap,
|
||||
animation-source routing and boss distinction. It does not validate the future
|
||||
42-Profession silhouette library, 100-enemy diversity, hit timing, gameplay
|
||||
state, engine performance, UI localization, touch, audio or device behavior.
|
||||
|
||||
The next bounded visual task is a separate VFX language proof over this scale,
|
||||
followed by compatible small-asset sheets. Bulk final content remains frozen
|
||||
until those proofs and Owner visual review pass.
|
||||
@@ -0,0 +1,27 @@
|
||||
8ae54ba0ddb391bc819bd457fcfd658cb2ac118644b44bcb00b1266f98655ec8 ./BATCH_CONTRACT.md
|
||||
5583909d73a52a93758fdc044c344213b8484ff7c3d243b2e1471e0fd1d91a82 ./VISUAL_REVIEW.md
|
||||
17d24ea469d19b5988a6742d6ace8459db53cd3158551cfae191650475fffc6e ./outputs/01_empty_battlefield.dry-run.json
|
||||
d8c1d2fb7fb466ef1abf8f9809ed9db276359d4905d260771ebd70be39fc3512 ./outputs/01_empty_battlefield.png
|
||||
43c80d6bb29afccdbd9271d960226557c9c8189ac4b9ea0219c78fbb525907c4 ./outputs/01_empty_battlefield.report.json
|
||||
746a5ea44d91d1d335fc6939b57edc16e23847fb326b6b7bb018760136fee107 ./outputs/battle-scale-proof-01/audit.json
|
||||
ff11c9b9b9fac89b089dda114b2d37e11f513bdcd179d8e60b41714039710e49 ./outputs/battle-scale-proof-01/battle-scale-contact.png
|
||||
eb93b7b8b2ef8300bf6675e5e1aacd2ea9189151352f402c1c4b2b326774fc2c ./outputs/battle-scale-proof-01/battle-scale-motion.mp4
|
||||
cc4970f79e87e54db07f0debe75d69648b8d04f2cb0b50b982636a4845fa1888 ./outputs/battle-scale-proof-01/battle-scale-opening.png
|
||||
cc4970f79e87e54db07f0debe75d69648b8d04f2cb0b50b982636a4845fa1888 ./outputs/battle-scale-proof-01/frames/frame-000.png
|
||||
24ae9b38d5b1d1b87a03a16fc23b6f501378dae7253f0e9d4dd131ced90b055f ./outputs/battle-scale-proof-01/frames/frame-001.png
|
||||
99ca3cfd414d5c348019c918125c8ed8c34ceb35fcfe1cb8cbdfa870f74b7919 ./outputs/battle-scale-proof-01/frames/frame-002.png
|
||||
0b7a4a56132f82247ded1f21b4631a57c27fad031ba71d132a35c07aa23b6147 ./outputs/battle-scale-proof-01/frames/frame-003.png
|
||||
2e5eb303ed51f97e5bbdd22ef6cbd5f490476b42a4a80d6917fb8ad710571f02 ./outputs/battle-scale-proof-01/frames/frame-004.png
|
||||
b855a8ac0fc8467d34874acc0ecbd043a90813bccf4d13067c272dbddba41f8a ./outputs/battle-scale-proof-01/frames/frame-005.png
|
||||
af3d0289c5c38855d022f1b92d3bd2eef22e9278a5bc6600776e7051624fceb6 ./outputs/battle-scale-proof-01/frames/frame-006.png
|
||||
785ec63e77b222b1bc6f781c1b7b84e6c52932e0425b8ed8b81e4271d78da959 ./outputs/battle-scale-proof-01/frames/frame-007.png
|
||||
eba64078d27757d1ee2fd20338e42d7e4b835a5e33f48439c6c57e0cb14e5ffe ./outputs/battle-scale-proof-01/frames/frame-008.png
|
||||
66acba2fc490c27c498e76bd9cdad3c3585a6afede0d3fb9b2d3902e47bfd4f0 ./outputs/battle-scale-proof-01/frames/frame-009.png
|
||||
d9618bab3cc559555f85780fa7b63454c7192b3dd7c23e6f4d349471f2cef58a ./outputs/battle-scale-proof-01/frames/frame-010.png
|
||||
05fb1331cab2b610cc1ddb524c5c49e8a680ceb669bb1bf767b8cc10ec192bee ./outputs/battle-scale-proof-01/frames/frame-011.png
|
||||
c2909d94c595bdabfe24b526e1e69d5e7746e74786ebeb3ceea7a4e39561fde9 ./outputs/battle-scale-proof-01/frames/frame-012.png
|
||||
737f7fbf2ecc4840a5791ee71cc101b9a574543a2e9c5eed63c218f4b29b1daf ./outputs/battle-scale-proof-01/frames/frame-013.png
|
||||
0b2f33a00e7e03848283e04716a6e91ee64865a0938e017f9985e3f6d9186509 ./outputs/battle-scale-proof-01/frames/frame-014.png
|
||||
8165bdd777e7007d8892ccda8aff29e0fcec93b2d36eb9363365086822808381 ./outputs/battle-scale-proof-01/frames/frame-015.png
|
||||
523bbf54ecfd0a10e68f06581a5c24e2f179a65770056a6de6b6925924fc1afe ./outputs/battle-scale-proof-01/video.sha256
|
||||
07a0e4e3a60963558111ce63c6d69c16790651b2896d3fb4cee94861181e1fa4 ./prompts/01_empty_battlefield.txt
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mode": "dry-run",
|
||||
"model": "gpt-image-2",
|
||||
"size": "16:9",
|
||||
"resolution": "2k",
|
||||
"output": "docs/prototype/generation/phase6_battle_scale_04/outputs/01_empty_battlefield.png",
|
||||
"local_references": [
|
||||
"docs/prototype/generation/phase6_ui_direction_01/outputs/05_battle_ui_japanese_scale.png"
|
||||
],
|
||||
"remote_reference_count": 0,
|
||||
"prompt_chars": 1699
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 MiB |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"mode": "execute",
|
||||
"model": "gpt-image-2",
|
||||
"size": "16:9",
|
||||
"resolution": "2k",
|
||||
"output": "docs/prototype/generation/phase6_battle_scale_04/outputs/01_empty_battlefield.png",
|
||||
"local_references": [
|
||||
"docs/prototype/generation/phase6_ui_direction_01/outputs/05_battle_ui_japanese_scale.png"
|
||||
],
|
||||
"remote_reference_count": 0,
|
||||
"prompt_chars": 1699,
|
||||
"task_id": "task_01KZSD6PE31P8512H61T0Q1Z0V",
|
||||
"status": "completed",
|
||||
"bytes": 3644787,
|
||||
"cost": 0.014,
|
||||
"credits_cost": 0.14
|
||||
}
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
@@ -0,0 +1 @@
|
||||
eb93b7b8b2ef8300bf6675e5e1aacd2ea9189151352f402c1c4b2b326774fc2c docs/prototype/generation/phase6_battle_scale_04/outputs/battle-scale-proof-01/battle-scale-motion.mp4
|
||||
@@ -0,0 +1,11 @@
|
||||
Create one EMPTY 16:9 production background master for the Aetherbound Guild landscape auto-battle screen.
|
||||
|
||||
Use the attached accepted battle-direction image only for Japanese 2D painted environment style, daylight palette, horizon depth, ruin materials, camera height and ground-plane perspective. Do not copy or retain any of its characters, creatures, interface, symbols, health bars, portraits, attacks or text.
|
||||
|
||||
Scene: a bright elevated aether ruin meadow on a long horizontal travel corridor. Soft blue sky, distant floating stone structures, chalk-white broken arches, pale mineral pillars, sparse repaired guild banners with NO emblem or writing, green grass and small field flowers. The lower third is a broad continuous walkable ground plane with quiet value contrast behind future small sprites. Keep the center collision corridor and both entry edges open. Put architectural interest in the upper and far-background layers, not in front of the combat line.
|
||||
|
||||
Japanese 2D game background painting, light thin structural linework, soft cel-paint integration, clear atmospheric depth, paper white, mineral blue, field green, weathered stone and restrained repair-gold accents. Bright readable daylight. No dark dashboard mood, no photorealism, no 3D render, no pixel art, no neon-purple wash, no dramatic bloom, no bokeh.
|
||||
|
||||
ABSOLUTELY EMPTY: no person, hero, recruit, soldier, enemy, monster, animal, spirit, face, silhouette, statue shaped like a person, vehicle, weapon, projectile, spell, particle, magic ring, impact mark, shadow shaped like a figure, UI, panel, button, icon, cursor, number, pseudo-text, letters, watermark or logo.
|
||||
|
||||
Output one opaque 2048x1152 landscape background only.
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compose accepted Phase 6 sprites into a deterministic battle-scale proof."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
CANVAS = (2048, 1152)
|
||||
FRAME_COUNT = 16
|
||||
FPS = 8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Actor:
|
||||
side: str
|
||||
state_dir: Path
|
||||
x: int
|
||||
bottom: int
|
||||
height: int
|
||||
phase: int
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--background", type=Path, required=True)
|
||||
parser.add_argument("--out-dir", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def frame_files(state_dir: Path) -> list[Path]:
|
||||
files = sorted(state_dir.glob("frame-*.png"), key=lambda path: int(path.stem.split("-")[-1]))
|
||||
if not files:
|
||||
raise SystemExit(f"no frames found: {state_dir}")
|
||||
return files
|
||||
|
||||
|
||||
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")
|
||||
bbox = alpha.point(lambda value: 255 if value >= 16 else 0).getbbox()
|
||||
if bbox is None:
|
||||
raise SystemExit(f"empty alpha frame: {path}")
|
||||
cropped = source.crop(bbox)
|
||||
scale = target_height / cropped.height
|
||||
width = max(1, round(cropped.width * scale))
|
||||
resized = cropped.resize((width, target_height), Image.Resampling.LANCZOS)
|
||||
return resized, bbox
|
||||
|
||||
|
||||
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)]
|
||||
sprite, source_bbox = fitted_sprite(source, actor.height)
|
||||
left = round(actor.x - sprite.width / 2)
|
||||
top = actor.bottom - sprite.height
|
||||
right = left + sprite.width
|
||||
bottom = top + sprite.height
|
||||
if left < 0 or top < 96 or right > CANVAS[0] or bottom > 944:
|
||||
raise SystemExit(f"actor outside battle safe region: {actor} -> {(left, top, right, bottom)}")
|
||||
canvas.alpha_composite(sprite, (left, top))
|
||||
return {
|
||||
"side": actor.side,
|
||||
"source": str(source),
|
||||
"source_sha256": sha256(source),
|
||||
"source_alpha_bbox": list(source_bbox),
|
||||
"content_rect": [left, top, right, bottom],
|
||||
"content_height": actor.height,
|
||||
}
|
||||
|
||||
|
||||
def draw_hud(canvas: Image.Image) -> None:
|
||||
draw = ImageDraw.Draw(canvas, "RGBA")
|
||||
ink = (24, 32, 45, 220)
|
||||
paper = (244, 240, 224, 235)
|
||||
blue = (70, 158, 217, 255)
|
||||
coral = (218, 77, 79, 255)
|
||||
gold = (194, 151, 67, 255)
|
||||
draw.rounded_rectangle((32, 24, 650, 92), radius=6, fill=ink, outline=gold, width=3)
|
||||
draw.rounded_rectangle((1398, 24, 2016, 92), radius=6, fill=ink, outline=gold, width=3)
|
||||
for index in range(10):
|
||||
x = 86 + index * 52
|
||||
draw.polygon([(x, 42), (x + 13, 55), (x, 68), (x - 13, 55)], fill=blue)
|
||||
for index in range(12):
|
||||
x = 1452 + index * 45
|
||||
draw.polygon([(x, 42), (x + 12, 54), (x, 66), (x - 12, 54)], fill=coral)
|
||||
draw.rounded_rectangle((410, 1010, 1638, 1132), radius=5, fill=ink, outline=gold, width=3)
|
||||
for index in range(10):
|
||||
left = 438 + index * 118
|
||||
draw.rounded_rectangle((left, 1028, left + 92, 1112), radius=4, fill=paper, outline=gold, width=2)
|
||||
draw.rectangle((left + 10, 1092, left + 82, 1102), fill=(76, 164, 101, 255))
|
||||
|
||||
|
||||
def actors(repo: Path) -> tuple[list[Actor], list[Actor], Actor]:
|
||||
motion = repo / "docs/prototype/generation/phase6_motion_proof_03/runs"
|
||||
recruit = motion / "recruit-aegis"
|
||||
enemy = motion / "enemy-riftblade"
|
||||
boss = motion / "boss-tempest-seraph"
|
||||
|
||||
ally_states = [
|
||||
recruit / "accepted/guard/frames",
|
||||
recruit / "accepted-r2/advance/frames",
|
||||
recruit / "accepted/basic_attack/frames",
|
||||
recruit / "accepted/idle/frames",
|
||||
recruit / "accepted-r2/profession_action/frames",
|
||||
recruit / "accepted-r2/advance/frames",
|
||||
recruit / "accepted/guard/frames",
|
||||
recruit / "accepted/hit/frames",
|
||||
recruit / "accepted/basic_attack/frames",
|
||||
recruit / "accepted/idle/frames",
|
||||
]
|
||||
allies = [
|
||||
Actor("ally", state, 112 + index * 94, 854 - (index % 2) * 10, 136 + (index % 3) * 6, index * 2)
|
||||
for index, state in enumerate(ally_states)
|
||||
]
|
||||
|
||||
enemy_states = [
|
||||
enemy / "accepted/telegraph/frames",
|
||||
enemy / "accepted/attack/frames",
|
||||
enemy / "accepted/idle/frames",
|
||||
enemy / "accepted/advance/frames",
|
||||
enemy / "accepted/hit/frames",
|
||||
]
|
||||
enemies: list[Actor] = []
|
||||
for index in range(12):
|
||||
depth = index % 3
|
||||
enemies.append(
|
||||
Actor(
|
||||
"enemy",
|
||||
enemy_states[index % len(enemy_states)],
|
||||
1120 + index * 48,
|
||||
838 - depth * 42,
|
||||
124 + (index % 4) * 5,
|
||||
index,
|
||||
)
|
||||
)
|
||||
boss_actor = Actor("boss", boss / "accepted/telegraph/frames", 1858, 842, 302, 0)
|
||||
return allies, enemies, boss_actor
|
||||
|
||||
|
||||
def make_contact(paths: list[Path], output: Path) -> None:
|
||||
thumbs: list[Image.Image] = []
|
||||
for path in paths:
|
||||
image = Image.open(path).convert("RGB")
|
||||
thumbs.append(image.resize((384, 216), Image.Resampling.LANCZOS))
|
||||
sheet = Image.new("RGB", (1920, 216), (20, 24, 30))
|
||||
for index, image in enumerate(thumbs):
|
||||
sheet.paste(image, (index * 384, 0))
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
sheet.save(output, optimize=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
repo = Path(__file__).resolve().parents[1]
|
||||
if not args.background.is_file():
|
||||
raise SystemExit(f"missing background: {args.background}")
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
frames_dir = args.out_dir / "frames"
|
||||
frames_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
background = Image.open(args.background).convert("RGB").resize(CANVAS, Image.Resampling.LANCZOS)
|
||||
allies, enemies, boss_template = actors(repo)
|
||||
frame_reports: list[dict[str, object]] = []
|
||||
frame_paths: list[Path] = []
|
||||
|
||||
for tick in range(FRAME_COUNT):
|
||||
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))
|
||||
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)
|
||||
actor_reports.append(paste_actor(canvas, boss_actor, tick % 8))
|
||||
for actor in sorted(allies, key=lambda item: item.bottom):
|
||||
actor_reports.append(paste_actor(canvas, actor, tick))
|
||||
draw_hud(canvas)
|
||||
output = frames_dir / f"frame-{tick:03d}.png"
|
||||
canvas.convert("RGB").save(output, optimize=True)
|
||||
frame_paths.append(output)
|
||||
frame_reports.append({"frame": tick, "actors": actor_reports, "sha256": sha256(output)})
|
||||
|
||||
opening = args.out_dir / "battle-scale-opening.png"
|
||||
opening.write_bytes(frame_paths[0].read_bytes())
|
||||
contact = args.out_dir / "battle-scale-contact.png"
|
||||
make_contact([frame_paths[index] for index in (0, 4, 8, 12, 15)], contact)
|
||||
audit = {
|
||||
"version": 1,
|
||||
"kind": "aetherbound-battle-scale-motion-proof",
|
||||
"canvas": list(CANVAS),
|
||||
"fps": FPS,
|
||||
"frames": FRAME_COUNT,
|
||||
"duration_seconds": FRAME_COUNT / FPS,
|
||||
"background": {"path": str(args.background), "sha256": sha256(args.background)},
|
||||
"counts_per_frame": {"allies": 10, "ordinary_enemies": 12, "bosses": 1},
|
||||
"height_contract": {"allies": [130, 155], "ordinary_enemies": [120, 145], "boss": [280, 315]},
|
||||
"frame_reports": frame_reports,
|
||||
"opening_sha256": sha256(opening),
|
||||
"contact_sha256": sha256(contact),
|
||||
}
|
||||
(args.out_dir / "audit.json").write_text(json.dumps(audit, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"ok": True, "frames": FRAME_COUNT, "fps": FPS, "out_dir": str(args.out_dir)}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||