art: prove crowded battle motion scale

This commit is contained in:
2026-08-12 06:02:57 +08:00
parent 18d7ca7809
commit a2f4d5adba
29 changed files with 7161 additions and 0 deletions
@@ -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
}
Binary file not shown.

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
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

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.
+221
View File
@@ -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()