"""Offline first-person atlas packing and original deterministic bowling Foley.""" from pathlib import Path import json import math import random import struct import wave from PIL import Image from pack_bowling import tween, pack ROOT = Path(__file__).resolve().parents[2] ART = ROOT/'assets/sprites/bowling' SOUND = ROOT/'assets/audio/bowling' def wav(name, seconds, pulses, rolling=False): rate = 22050 rng = random.Random(41309) samples = [] low = 0.0 for i in range(round(rate*seconds)): t = i/rate noise = rng.uniform(-1,1) low += .12*(noise-low) value = 0.0 if rolling: envelope = min(1,t/.03,(seconds-t)/.03) value = .22*low*envelope*(.7+.3*math.sin(t*43)) for start, gain, frequency in pulses: age = t-start if 0 <= age < .45: value += gain*(.42*math.sin(age*frequency*math.tau)*math.exp(-age*27) + .58*noise*math.exp(-age*45)) samples.append(max(-32767,min(32767,round(value*24000)))) with wave.open(str(SOUND/name),'wb') as output: output.setparams((1,2,rate,0,'NONE','not compressed')) output.writeframes(struct.pack(f'<{len(samples)}h',*samples)) def pinsetter_audio(): wav('pinsetter.wav',5.2,[(0,.24,120),(.4,.22,160),(1.7,.3,105), (2.2,.24,130),(3.7,.2,105),(4,.22,160),(4.4,.25,140)],True) def return_audio(): wav('return.wav',2.0,[(.35,.18,95),(.75,.12,130),(1.3,.16,160), (1.65,.32,110),(1.78,.18,85)],True) def main(): SOUND.mkdir(parents=True,exist_ok=True) source = Image.open(ART/'player-underarm-v2.png').convert('RGBA') keys = [] # Cell 7 contains a detached ball: release uses the actual simulated sphere. for cell in (*range(7),8,9,10,11): x,y = cell%4,cell//4 keys.append(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)) frames=[] for i,a in enumerate(keys): b=keys[min(i+1,len(keys)-1)] # Never morph the held ball into an empty hand across the release event. frames += [a,a,a] if i==6 else [a,tween(a,b,1/3),tween(a,b,2/3)] pack('player-underarm-v2.bin',frames) frames[0].save(ART/'player-underarm-v2.gif',save_all=True, append_images=frames[1:],duration=33,loop=0,disposal=2) wav('release.wav',.35,[(0,.8,94),(.025,.3,130)]) wav('rolling.wav',14.5,[],True) wav('pins-light.wav',.7,[(0,.65,430),(.08,.38,670),(.19,.24,510)]) wav('pins-heavy.wav',1.3,[(0,.9,390),(.025,.75,710),(.08,.65,510), (.14,.6,830),(.24,.4,620),(.39,.32,440),(.57,.2,740)]) wav('applause.wav',1.4,[(i*.09,.25+(i%3)*.04,1700+i*23) for i in range(13)]) pinsetter_audio() return_audio() (ART/'player-underarm-v2.json').write_text(json.dumps({ 'generator':'built-in image_gen','source':'player-underarm-v2.png', 'brief':'First-person right-handed underarm bowling: low pendulum swing beside thigh, ' 'backswing leaves view, hand behind/below blue ball, ankle-height release, palm-up follow-through; ' 'dark rolled sleeve; transparent 4 by 3 atlas. Replaces rejected forward-pushing draft.', 'selected_cells':[0,1,2,3,4,5,6,8,9,10,11], 'frames':len(frames),'frame_size':[256,256],'release_frame':21, 'audio':'Original seeded PCM synthesis, no sampled third-party recordings.'},indent=2)+'\n') print('Packed',len(frames),'underarm frames and seven original PCM effects') if __name__=='__main__': main()