63 lines
3.3 KiB
Python
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()
|