- Bowling feel: impulse contact solver, string pinsetter cycle, in-world lengthwise return rack and visible ball return lift - Ball weights: 4/5/6/7 kg selection with equal launch work physics, momentum balance, and projected picking - Audio: dedicated MCI channels with 7 synthesized waveaudio cues (release, rolling, impacts, applause, pinsetter, return) - First-person animations: 33-frame player underarm throw/follow-through sequence (player-underarm-v2) - Visitors: 10 persistent shared cast identities with exclusive reservations, activity transitions, and chair sit/rise animations - Tests & verification: comprehensive native regression test suites and updated documentation - Synchronize binaries and update launcher hash to 536E23BED4055562BC63871826FF44FFED037923A9BE8E0F591131DB4FBD104B
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""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()
|