KAZIK/tools/generators/pack_bowling_feel.py
Artvasil09 7e081beeca feat: enhance bowling physics, audio, ball weight selection, and visitor cast routines
- 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
2026-11-04 18:44:12 +03:00

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