"""Offline bowling art, furniture and collision data. Runtime is native MASM.""" from pathlib import Path import json import struct import cv2 import numpy as np from PIL import Image ROOT = Path(__file__).resolve().parents[2] ART = ROOT/'assets/sprites/bowling' OUT = ROOT/'assets/animations/bowling' def pack(name, frames): w,h = frames[0].size data = b''.join(f.tobytes('raw','BGRA') for f in frames) (OUT/name).write_bytes(struct.pack('<4s4I',b'ANM1',w,h,len(frames),len(data))+data) def cells(name, columns, rows, seated=False): source = Image.open(ART/name).convert('RGBA') frames = [] for y in range(rows): for x in range(columns): cell = source.crop((round(x*source.width/columns),round(y*source.height/rows), round((x+1)*source.width/columns),round((y+1)*source.height/rows))) a = np.asarray(cell).copy() a[:,:,3] = np.where(a[:,:,3] >= 128,255,0) # Keep the actor, not detached released balls or neighbour-cell fragments. if not seated: count,labels,stats,_ = cv2.connectedComponentsWithStats((a[:,:,3]>0).astype(np.uint8)) keep = 1+int(np.argmax(stats[1:,cv2.CC_STAT_AREA])) a[:,:,3] = np.where(labels == keep,255,0) cell = Image.fromarray(a) frames.append(cell) scale = min(240/max(f.getbbox()[3]-f.getbbox()[1] for f in frames), 248/max(f.width for f in frames)) result = [] for cell in frames: box = cell.getbbox() resized = cell.resize((round(cell.width*scale),round(cell.height*scale)),Image.Resampling.LANCZOS) frame = Image.new('RGBA',(256,256)) frame.paste(resized,((256-resized.width)//2,250-round(box[3]*scale))) result.append(frame) return result def tween(a,b,t): a,b = np.asarray(a),np.asarray(b) ga,gb = cv2.cvtColor(a,cv2.COLOR_RGBA2GRAY),cv2.cvtColor(b,cv2.COLOR_RGBA2GRAY) fa = cv2.calcOpticalFlowFarneback(ga,gb,None,.5,4,25,4,5,1.2,0) fb = cv2.calcOpticalFlowFarneback(gb,ga,None,.5,4,25,4,5,1.2,0) xy = np.mgrid[:256,:256][::-1].transpose(1,2,0).astype(np.float32) wa = cv2.remap(a,xy-fa*t,None,cv2.INTER_LINEAR,borderMode=cv2.BORDER_CONSTANT) wb = cv2.remap(b,xy-fb*(1-t),None,cv2.INTER_LINEAR,borderMode=cv2.BORDER_CONSTANT) return Image.fromarray(np.clip(wa*(1-t)+wb*t,0,255).astype(np.uint8)) def main(): OUT.mkdir(parents=True,exist_ok=True) source = Image.open(ART/'materials-v1.png').convert('RGBA') materials = [source.crop((round(x*source.width/4),round(y*source.height/3), round((x+1)*source.width/4),round((y+1)*source.height/3))) .resize((256,256),Image.Resampling.LANCZOS) for y in range(3) for x in range(4)] materials += [Image.new('RGBA',(256,256),(40,210,208,255)), Image.new('RGBA',(256,256),(229,79,65,255))] pack('materials-v1.bin',materials) man,woman = cells('bowler-man-v1.png',4,4),cells('bowler-woman-v1.png',4,2) actors = [] for poses in (man,woman): motion = [] for a,b in zip(poses[:7],poses[1:8]): motion += [a,tween(a,b,1/3),tween(a,b,2/3)] motion += [poses[7]]*3 extras = poses[8:16] if len(poses) == 16 else [poses[0]]*4+[poses[7]]*4 actors += motion+extras pack('bowlers-v1.bin',actors) pack('seated-v1.bin',cells('seated-v1.png',4,2,True)) preview = Image.new('RGBA',(8*256,2*256)) for i,f in enumerate(actors[:24:3]+actors[32:56:3]): preview.paste(f,((i%8)*256,(i//8)*256)) preview.save(ART/'bowling-preview.png') props,collisions,seats = [],[],[] def box(a,b,c,d,e,f,m,solid=False): props.append((a,b,c,d,e,f,m)) if solid: collisions.append((a-.17,b+.17,c-.17,d+.17)) def sofa(x,y,w=2.8): box(x-w/2,x+w/2,y-.45,y+.45,.08,.3,236,True) box(x-w/2+.1,x+w/2-.1,y-.43,y+.36,.3,.4,235) box(x-w/2,x+w/2,y+.3,y+.52,.3,.85,235) for dx in (-w/2,w/2-.16): box(x+dx,x+dx+.16,y-.45,y+.45,.3,.56,240) seats.append((x,y-.1,384)) for pair in range(4): x = 3.95+3.8*pair sofa(x,28.1) box(x-.6,x+.6,26.7,27.25,.38,.46,237,True) box(x-.12,x+.12,26.85,27.05,0,.4,238) box(x-.38,x+.38,24.8,26,.05,.62,236,True) box(x-.33,x+.33,24.85,25.8,.62,.72,238) for x in (22,27): for y in (12,18): box(x-1,x+1,y-.75,y+.75,.48,.55,237,True) box(x-.1,x+.1,y-.1,y+.1,0,.49,238) sofa(x,y+1.55,2.2) box(24,30,4.5,5.5,0,1.05,236,True) box(23.9,30.1,4.4,5.6,1.05,1.16,237) box(24,30,1.15,1.45,0,3.1,236,True) for z in (.9,1.7,2.5): box(24,30,1.15,1.8,z,z+.08,238) for x in (25,27,29): box(x-.08,x+.08,6.7,6.86,0,.8,238,True) box(x-.3,x+.3,6.5,7.1,.8,.9,240) seats.append((x,6.8,384)) box(19.1,19.3,1,24.8,0,.9,236,True) box(19.05,19.35,1,24.8,.9,1,238) # Floor-standing speakers and the pinsetter housings have real side/back faces. for x in (1.45,18.5): box(x-.3,x+.3,2,2.7,0,2.8,233) data = ['; Generated by pack_bowling.py.','BOWL_PROP_COUNT equ '+str(len(props)),'.data', 'bwProps3D LABEL DWORD'] for p in props: data.append(' dd '+','.join(str(round(v*65536)) for v in p[:6])+','+str(p[6])) data += ['BOWL_COLLISION_COUNT equ '+str(len(collisions)),'bwCollision3D LABEL DWORD'] data += [' dd '+','.join(str(round(v*65536)) for v in p) for p in collisions] data += ['BOWL_SEAT_COUNT equ '+str(len(seats)),'bwSeats3D LABEL DWORD'] data += [' dd '+','.join([str(round(p[0]*65536)),str(round(p[1]*65536)),str(p[2])]) for p in seats] room = [[232 if x in (0,31) or y in (0,31) else 0 for x in range(32)] for y in range(32)] data += ['bwMap3D LABEL BYTE'] data += [' db '+','.join(map(str,row)) for row in room] data += ['.code'] (ROOT/'src_graphics/bowling_layout.inc').write_text('\n'.join(data)+'\n',encoding='ascii') (ART/'packed-v1.json').write_text(json.dumps({'materials':[256,256,14], 'bowlers':[256,256,64],'seated':[256,256,8],'lanes':8,'active_lanes':[0,3,6], 'room':[30,30,6],'props':len(props),'collisions':collisions,'seats':seats},indent=2)+'\n') print('Packed bowling:',len(props),'solid prop parts, 64 whole-body motion frames, 8 seated poses') if __name__ == '__main__': main()