"""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()