KAZIK/tools/tests/check_character_cast.py

63 lines
3.3 KiB
Python

"""Shared sprite invariants and real MASM framebuffer captures."""
from pathlib import Path
import json
import struct
from PIL import Image, ImageChops
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT/'build/hall-expansion'
def check():
manifest = json.loads((ROOT/'assets/sprites/characters/cast-v1.json').read_text())
raw = (ROOT/'assets/animations/characters/cast-v1.bin').read_bytes()
magic,w,h,count,size = struct.unpack_from('<4s4I',raw)
assert (magic,w,h,count) == (b'ANM1',192,224,1000)
assert size == len(raw)-20 == w*h*count*4
assert len(manifest['characters']) == 10 and manifest['stride'] == 100
def frame(n):
offset = 20+n*w*h*4
return Image.frombytes('RGBA',(w,h),raw[offset:offset+w*h*4],'raw','BGRA')
assert len({frame(i*100).tobytes() for i in range(10)}) == 10
roster = Image.new('RGB',(320*5,240*2),(28,34,40))
for identity in range(10):
start = identity*100
standing = frame(start).getbbox()
seated = frame(start+12).getbbox()
assert seated[3]-seated[1] < (standing[3]-standing[1])*.87,(identity,'seated body resized')
for offset in range(100):
box = frame(start+offset).getbbox()
assert box and box[0] > 0 and box[2] < w and box[3] <= 222,(identity,offset,box)
for direction in (24,32,40,48):
poses = [frame(start+direction+i) for i in range(8)]
assert len({p.crop((0,120,w,h)).tobytes() for p in poses}) == 8,(identity,direction,'frozen legs')
# Distinct hashes alone missed the old repeated wide-stride source poses.
spans = []
for index in range(8):
box = frame(start+40+index).getchannel('A').crop((0,180,w,h)).getbbox()
spans.append(box[2]-box[0])
assert max(spans)-min(spans) > 55,(identity,'missing passing pose',spans)
for i in range(8):
assert frame(start+40+i).transpose(Image.Transpose.FLIP_LEFT_RIGHT).tobytes() == frame(start+48+i).tobytes()
assert len({frame(start+72+i).tobytes() for i in range(24)}) >= 20
for action in range(6):
native = Image.open(OUT/f'cast-{identity}-{action}.bmp').convert('RGB')
bg = Image.new('RGB',native.size,(28,34,40))
assert ImageChops.difference(native,bg).getbbox(),(identity,action,'blank native pose')
if action == 0:
box = ImageChops.difference(native,bg).getbbox()
assert 100 <= box[3]-box[1] <= 170 and 215 <= box[1] <= 250,(identity,'visitor scale',box)
native.save(OUT/f'cast-{identity}-{action}.png')
if action == 0:
native = native.resize((320,240),Image.Resampling.LANCZOS)
roster.paste(native,((identity%5)*320,(identity//5)*240))
walking = [Image.open(OUT/f'cast-walk-{identity}-{phase}.bmp').convert('RGB') for phase in range(8)]
assert len({im.tobytes() for im in walking}) == 8,(identity,'native frozen gait')
walking[0].save(OUT/f'cast-walk-{identity}.gif',save_all=True,
append_images=walking[1:],duration=110,loop=0)
roster.save(OUT/'cast-native-roster.png')
print('PASS: ten identities, contact/passing gait, eye-level scale, 60 poses and 80 native walking frames')
if __name__ == '__main__':
check()