Widen centre drain gap; calm the music down
Gameplay (you should now be able to lose balls): - flipper length 170 -> 154, so the rest-position tips meet with a ~50px gap instead of ~18px - balls dropping down the middle now drain freely - keep the outlane guard posts so side drains stay fair (all drains now happen through the widened centre), and verify with autoplay: centre drains = 40, side = 0, ~10s average ball life for the bot Audio (was a harsh machine-gun): - completely rewrote both music loops: no more square waves or noise hats in the music; sparse triangle bass + soft melody with rests, at lower volume (play loop now ~47% audible windows, was a solid wall) - softened the wall-hit thud (frequent during rallies) and the charge ticks
@@ -525,8 +525,8 @@ class Game:
|
||||
if self.charging:
|
||||
self.was_charging = True
|
||||
self.charge = min(1.0, self.charge + 0.022)
|
||||
if self.frame % 5 == 0:
|
||||
self.sfx.play("tick", vol=0.3 + self.charge * 0.5)
|
||||
if self.frame % 6 == 0:
|
||||
self.sfx.play("tick", vol=0.16 + self.charge * 0.25)
|
||||
if self.charge >= 1.0:
|
||||
self._fire()
|
||||
else:
|
||||
@@ -628,9 +628,9 @@ class Game:
|
||||
elif e.kind == "nudge":
|
||||
self.sfx.play("tick", vol=0.2)
|
||||
elif e.kind == "wall_hit":
|
||||
if self.wall_cool <= 0 and e.data > 9.5:
|
||||
self.sfx.play("wall", vol=min(0.5, (e.data - 9) / 5))
|
||||
self.wall_cool = 5
|
||||
if self.wall_cool <= 0 and e.data > 11.0:
|
||||
self.sfx.play("wall", vol=min(0.4, (e.data - 11) / 6.0))
|
||||
self.wall_cool = 9
|
||||
elif e.kind == "flipper_hit":
|
||||
if self.rng.random() < 0.3:
|
||||
self.sfx.play("tick", vol=0.25)
|
||||
|
||||
@@ -17,7 +17,7 @@ MAX_SPEED = 13.0 # speed cap (prevents tunneling; ~300px of rise)
|
||||
RESTITUTION = 0.42
|
||||
BALL_R = 7.0
|
||||
|
||||
FLIP_LEN = 170.0
|
||||
FLIP_LEN = 154.0
|
||||
FLIP_REST = math.radians(6.0) # tips slightly lowered toward center drain
|
||||
FLIP_UP = -math.radians(52.0) # fired position
|
||||
FLIP_PIVOT_L = (62.0, 338.0)
|
||||
@@ -36,10 +36,11 @@ BUMPERS = [
|
||||
SLING_KICK_L = (0.60, -0.80)
|
||||
SLING_KICK_R = (-0.60, -0.80)
|
||||
SLINGS = [
|
||||
# upper slingshots by the lower bumpers - kick toward table center
|
||||
# upper slingshots just below the two outer bumpers - kick toward centre
|
||||
dict(pos=(150.0, 282.0), r=11.0, kick=SLING_KICK_L),
|
||||
dict(pos=(330.0, 282.0), r=11.0, kick=SLING_KICK_R),
|
||||
# guard posts at the outlane mouths - protect the corners
|
||||
# guard posts at the outlane mouths: wall-huggers get bounced back into
|
||||
# play instead of silently draining - the centre gap is the real risk
|
||||
dict(pos=(44.0, 314.0), r=12.0, kick=(0.80, -0.60)),
|
||||
dict(pos=(436.0, 314.0), r=12.0, kick=(-0.80, -0.60)),
|
||||
]
|
||||
@@ -137,7 +138,7 @@ class Table:
|
||||
for b in BUMPERS]
|
||||
self.slings = [
|
||||
dict(x=s["pos"][0], y=s["pos"][1], r=s["r"], cool=0, flash=0,
|
||||
kick=s["kick"], guard=(i >= 2))
|
||||
kick=s["kick"])
|
||||
for i, s in enumerate(SLINGS)]
|
||||
self.stars = [dict(x=x, y=y, r=13.0, lit=False, cool=0, pulse=0)
|
||||
for x, y in STARS]
|
||||
|
||||
@@ -113,9 +113,9 @@ class Synth:
|
||||
|
||||
# ---- all effects -----------------------------------------------------
|
||||
def _build_all(self):
|
||||
self._snd("flipL", self.tone(180, 0.07, 0.5, "square", slide=0.7))
|
||||
self._snd("flipR", self.tone(210, 0.07, 0.5, "square", slide=0.7))
|
||||
self._snd("wall", self.tone(90, 0.045, 0.18, "square", duty=0.3))
|
||||
self._snd("flipL", self.tone(180, 0.07, 0.4, "square", slide=0.7))
|
||||
self._snd("flipR", self.tone(210, 0.07, 0.4, "square", slide=0.7))
|
||||
self._snd("wall", self.tone(140, 0.05, 0.09, "tri"))
|
||||
self._snd("launch", self._mix(
|
||||
self.noise(0.28, 0.5, lp=0.25),
|
||||
self.tone(160, 0.3, 0.35, "saw", slide=1.9)))
|
||||
@@ -162,43 +162,70 @@ class Synth:
|
||||
|
||||
self._build_music()
|
||||
|
||||
def _build_music(self):
|
||||
"""Two short generative loops: a dreamy menu loop and a bouncy play loop."""
|
||||
# menu loop: soft arpeggio C - Am - F - G
|
||||
chords = [["C", "E", "G"], ["A", "C", "E"], ["F", "A", "C"], ["G", "B", "D"]]
|
||||
bass = [("C", 2), ("A", 2), ("F", 2), ("G", 2)]
|
||||
beat = 0.26
|
||||
parts = []
|
||||
for ci in range(4):
|
||||
n = int(SR * beat * 2)
|
||||
parts.append(self.tone(freq(bass[ci][0], bass[ci][1]), beat * 2.1, 0.22, "tri"))
|
||||
# melodic ripple over the chord
|
||||
for k in range(4):
|
||||
note = chords[ci][k % 3]
|
||||
f0 = freq(note, 4 if k < 2 else 5)
|
||||
parts.append(self.tone(f0, beat * 0.8, 0.14, "square", duty=0.4))
|
||||
parts.append(array.array("h", [0]) * int(SR * beat * 0.2))
|
||||
self._menu_music = self._mix(*parts)
|
||||
# ------------------------------------------------------------------ music
|
||||
def _music_buffer(self, tracks, total_s):
|
||||
"""Render time-placed notes (start_s, note, octave, dur_s, vol, shape)
|
||||
into one softly-mixed, normalized buffer."""
|
||||
n = int(SR * total_s)
|
||||
buf = [0] * n
|
||||
for (t0, nm, oc, dur, vol, shape) in tracks:
|
||||
samples = self.tone(freq(nm, oc), dur, vol, shape)
|
||||
off = int(SR * t0)
|
||||
for i, v in enumerate(samples):
|
||||
j = off + i
|
||||
if j < n:
|
||||
buf[j] = max(-32767, min(32767, buf[j] + v))
|
||||
peak = max(1, max(abs(v) for v in buf))
|
||||
sc = 28000.0 / peak if peak > 28000 else 1.0
|
||||
return array.array("h", (int(v * sc) for v in buf))
|
||||
|
||||
# play loop: bouncy 8th-note line C-C-G-G-A-A-G
|
||||
line = [("C", 4), ("C", 4), ("G", 4), ("G", 4), ("A", 4), ("A", 4), ("G", 4), ("E", 4)]
|
||||
bassp = [("C", 2), ("C", 2), ("G", 2), ("G", 2), ("A", 2), ("A", 2), ("E", 2), ("E", 2)]
|
||||
step = 0.16
|
||||
parts = []
|
||||
def _build_music(self):
|
||||
"""Two short generative loops, deliberately sparse & soft: a calm menu
|
||||
loop and a gentle bouncy play loop. (Square waves and hats are used in
|
||||
SFX only - never in the music - to keep it from getting harsh.)"""
|
||||
|
||||
# ---- menu: dreamy, mostly rests, C - Am - F - G ------------------------
|
||||
seq = [
|
||||
(("C", 2), ["C", "E", "G", "D"], "E"),
|
||||
(("A", 2), ["A", "C", "E", "B"], "C"),
|
||||
(("F", 2), ["F", "A", "C", "G"], "A"),
|
||||
(("G", 2), ["G", "B", "D", "A"], "B"),
|
||||
]
|
||||
tracks = []
|
||||
CH = 1.8
|
||||
for ci, (bass, arp, twinkle) in enumerate(seq):
|
||||
t = ci * CH
|
||||
tracks.append((t, bass[0], bass[1], CH * 0.72, 0.12, "tri")) # bass
|
||||
for k in range(4): # arpeggio
|
||||
oc = 4 if k < 2 else 5
|
||||
tracks.append((t + 0.35 + k * 0.26, arp[k], oc, 0.24, 0.08, "tri"))
|
||||
tracks.append((t + 1.3, twinkle, 5, 0.55, 0.045, "tri")) # twinkle
|
||||
menu_total = 4 * CH + 0.8
|
||||
self._menu_music = self._music_buffer(tracks, menu_total)
|
||||
|
||||
# ---- play: bouncy but gentle, 16 eighth-notes @ ~0.30s ----------------
|
||||
step = 0.30
|
||||
bass_seq = [("C", 2), ("C", 2), ("G", 2), ("G", 2),
|
||||
("A", 2), ("A", 2), ("F", 2), ("G", 2),
|
||||
("C", 2), ("C", 2), ("G", 2), ("G", 2),
|
||||
("A", 2), ("A", 2), ("G", 2), ("C", 2)]
|
||||
lead_seq = [None, None, ("E", 4), None, ("G", 4), None, ("E", 4), None,
|
||||
("D", 4), None, ("F", 4), None, ("A", 4), None, ("G", 4), None]
|
||||
tracks = []
|
||||
for i in range(16):
|
||||
nm, oc = line[i % 8]
|
||||
bn, bo = bassp[i % 8]
|
||||
parts.append(self.tone(freq(bn, bo), step * 1.8, 0.28, "tri"))
|
||||
parts.append(self.tone(freq(nm, oc), step * 1.3, 0.18, "square", duty=0.4))
|
||||
if i % 2 == 1:
|
||||
parts.append(self.noise(0.03, 0.10, lp=0.15))
|
||||
else:
|
||||
parts.append(array.array("h", [0]) * int(SR * step * 0.3))
|
||||
self._play_music = self._mix(*parts)
|
||||
t = i * step
|
||||
if i % 2 == 0: # bass on the beat
|
||||
bn, bo = bass_seq[i]
|
||||
tracks.append((t, bn, bo, step * 1.25, 0.15, "tri"))
|
||||
if lead_seq[i]: # light melody
|
||||
nm, oc = lead_seq[i]
|
||||
tracks.append((t + 0.02, nm, oc, step * 0.85, 0.085, "tri"))
|
||||
play_total = 16 * step + 1.2
|
||||
self._play_music = self._music_buffer(tracks, play_total)
|
||||
self._menu = pygame.mixer.Sound(buffer=self._menu_music.tobytes())
|
||||
self._play = pygame.mixer.Sound(buffer=self._play_music.tobytes())
|
||||
self._menu.set_volume(0.5)
|
||||
self._play.set_volume(0.45)
|
||||
self._menu.set_volume(0.40)
|
||||
self._play.set_volume(0.30)
|
||||
|
||||
# ---- playback --------------------------------------------------------
|
||||
def play(self, name, vol=1.0, pitch_var=0):
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |