Fix right flipper: mirror its geometry and sweep it per-substep
The right flipper's arm was computed with the same +cos(angle) direction as the left one, so it pointed into the right wall, off the 480px canvas - only the left paddle was ever visible/playable. - add physics.flipper_tip() which mirrors the right flipper (and use it in both collision and rendering) - derive the flipper's surface velocity from actual tip displacement (scaled by contact radius), instead of a hand-signed angular formula - advance flipper angles inside the collision substeps (6/frame while pressed) so a fast sweep can't jump past a resting ball; resting balls now get a real 'scoop' from either paddle - verify: pixel probe shows both paddles on screen (L=1007, R=999 blue px); physics probe confirms each paddle launches a resting ball
@@ -9,7 +9,7 @@ import pygame
|
|||||||
from physics import (CV_W, CV_H, PLAY_L, PLAY_R, PLAY_T, PLAY_B, BALL_R,
|
from physics import (CV_W, CV_H, PLAY_L, PLAY_R, PLAY_T, PLAY_B, BALL_R,
|
||||||
CENTER_HOLE, OUTLANE_L, OUTLANE_R, DRAIN_Y, LAUNCH_DIR,
|
CENTER_HOLE, OUTLANE_L, OUTLANE_R, DRAIN_Y, LAUNCH_DIR,
|
||||||
LAUNCH_MIN, LAUNCH_MAX, Table, FLIP_PIVOT_L, FLIP_PIVOT_R,
|
LAUNCH_MIN, LAUNCH_MAX, Table, FLIP_PIVOT_L, FLIP_PIVOT_R,
|
||||||
FLIP_RADIUS, FLIP_LEN)
|
FLIP_RADIUS, FLIP_LEN, flipper_tip)
|
||||||
import font as pix
|
import font as pix
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -195,9 +195,7 @@ def draw_sling(surf, s):
|
|||||||
|
|
||||||
def draw_flipper(surf, f, side):
|
def draw_flipper(surf, f, side):
|
||||||
px, py = f["pivot"]
|
px, py = f["pivot"]
|
||||||
a = f["angle"]
|
tipx, tipy = flipper_tip(f)
|
||||||
tipx = px + math.cos(a) * f["length"]
|
|
||||||
tipy = py + math.sin(a) * f["length"]
|
|
||||||
p0 = (int(px), int(py))
|
p0 = (int(px), int(py))
|
||||||
p1 = (int(tipx), int(tipy))
|
p1 = (int(tipx), int(tipy))
|
||||||
# shadow
|
# shadow
|
||||||
@@ -210,7 +208,9 @@ def draw_flipper(surf, f, side):
|
|||||||
pygame.draw.circle(surf, C["flipper_d"], (ex, ey), 6)
|
pygame.draw.circle(surf, C["flipper_d"], (ex, ey), 6)
|
||||||
pygame.draw.circle(surf, C["flipper"], (ex, ey), 4)
|
pygame.draw.circle(surf, C["flipper"], (ex, ey), 4)
|
||||||
# cute stripe near the top edge
|
# cute stripe near the top edge
|
||||||
nx, ny = -math.sin(a), math.cos(a)
|
a = f["angle"]
|
||||||
|
side_sign = -1.0 if side == "R" else 1.0
|
||||||
|
nx, ny = -math.sin(a) * side_sign, math.cos(a)
|
||||||
hx, hy = int(px + nx * 3), int(py + ny * 3)
|
hx, hy = int(px + nx * 3), int(py + ny * 3)
|
||||||
tx, ty = int(tipx + nx * 3), int(tipy + ny * 3)
|
tx, ty = int(tipx + nx * 3), int(tipy + ny * 3)
|
||||||
pygame.draw.line(surf, C["flipper_l"], (hx, hy), (tx, ty), 2)
|
pygame.draw.line(surf, C["flipper_l"], (hx, hy), (tx, ty), 2)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ FLIP_UP = -math.radians(52.0) # fired position
|
|||||||
FLIP_PIVOT_L = (62.0, 338.0)
|
FLIP_PIVOT_L = (62.0, 338.0)
|
||||||
FLIP_PIVOT_R = (418.0, 338.0)
|
FLIP_PIVOT_R = (418.0, 338.0)
|
||||||
FLIP_RADIUS = 5.5
|
FLIP_RADIUS = 5.5
|
||||||
FLIP_SNAP_UP = 0.40 # rad/frame while firing
|
FLIP_SNAP_UP = 0.36 # rad/frame while firing
|
||||||
FLIP_SNAP_DOWN = 0.05 # gentle return speed
|
FLIP_SNAP_DOWN = 0.05 # gentle return speed
|
||||||
FLIP_BOUNCE = 0.12
|
FLIP_BOUNCE = 0.12
|
||||||
|
|
||||||
@@ -72,6 +72,20 @@ def seg_info(px, py, ax, ay, bx, by):
|
|||||||
return t, cx, cy, dx * dx + dy * dy
|
return t, cx, cy, dx * dx + dy * dy
|
||||||
|
|
||||||
|
|
||||||
|
def flipper_tip(f):
|
||||||
|
"""World-space position of the flipper tip.
|
||||||
|
|
||||||
|
Angles are expressed in *local* coordinates: a positive angle lowers the
|
||||||
|
tip toward the centre drain (screen-down is +y). The right flipper is the
|
||||||
|
mirror image of the left, so its horizontal component is negated.
|
||||||
|
"""
|
||||||
|
px, py = f["pivot"]
|
||||||
|
a = f["angle"]
|
||||||
|
if f["side"] == "L":
|
||||||
|
return (px + math.cos(a) * f["length"], py + math.sin(a) * f["length"])
|
||||||
|
return (px - math.cos(a) * f["length"], py + math.sin(a) * f["length"])
|
||||||
|
|
||||||
|
|
||||||
class Ball:
|
class Ball:
|
||||||
__slots__ = ("x", "y", "vx", "vy", "rad", "prev_x", "prev_y")
|
__slots__ = ("x", "y", "vx", "vy", "rad", "prev_x", "prev_y")
|
||||||
|
|
||||||
@@ -133,6 +147,9 @@ class Table:
|
|||||||
dict(side="R", pivot=FLIP_PIVOT_R, angle=FLIP_REST, rest=FLIP_REST,
|
dict(side="R", pivot=FLIP_PIVOT_R, angle=FLIP_REST, rest=FLIP_REST,
|
||||||
up=FLIP_UP, length=FLIP_LEN, pressed=False, w=0.0, contacting=False),
|
up=FLIP_UP, length=FLIP_LEN, pressed=False, w=0.0, contacting=False),
|
||||||
]
|
]
|
||||||
|
for f in self.flippers:
|
||||||
|
f["prev_tip"] = flipper_tip(f)
|
||||||
|
f["tipvx"] = f["tipvy"] = 0.0
|
||||||
self.cannon = dict(x=CANNON[0], y=CANNON[1], r=CANNON_R)
|
self.cannon = dict(x=CANNON[0], y=CANNON[1], r=CANNON_R)
|
||||||
|
|
||||||
self.ball = Ball(CANNON[0], CANNON[1])
|
self.ball = Ball(CANNON[0], CANNON[1])
|
||||||
@@ -204,20 +221,15 @@ class Table:
|
|||||||
|
|
||||||
ball = self.ball
|
ball = self.ball
|
||||||
|
|
||||||
# flipper angle update (kinematic; per-frame, then substepped)
|
# flippers move inside substeps; sweep 6 steps/frame while pressed so
|
||||||
for f in self.flippers:
|
# the paddle never jumps over a resting ball
|
||||||
target = f["up"] if f["pressed"] else f["rest"]
|
flips_moving = any(f["pressed"] for f in self.flippers)
|
||||||
rate = FLIP_SNAP_UP if f["pressed"] else FLIP_SNAP_DOWN
|
|
||||||
d = target - f["angle"]
|
|
||||||
f["angle"] = target if abs(d) < rate else f["angle"] + (rate if d > 0 else -rate)
|
|
||||||
f["w"] = f["angle"] - (f.get("prev_angle", f["angle"]))
|
|
||||||
f["prev_angle"] = f["angle"]
|
|
||||||
f["contacting"] = False
|
|
||||||
|
|
||||||
speed = ball.speed
|
speed = ball.speed
|
||||||
n_sub = 1 if speed <= 0.0 else min(6, max(1, int(math.ceil(speed / 4.0))))
|
n_sub = 6 if flips_moving else 1
|
||||||
|
if speed > 0.0:
|
||||||
|
n_sub = max(n_sub, min(6, int(math.ceil(speed / 4.0))))
|
||||||
for _ in range(n_sub):
|
for _ in range(n_sub):
|
||||||
self._substep(ball, 1.0 / n_sub)
|
self._substep(ball, 1.0 / n_sub, n_sub)
|
||||||
|
|
||||||
self.trail.append((ball.x, ball.y))
|
self.trail.append((ball.x, ball.y))
|
||||||
if len(self.trail) > 9:
|
if len(self.trail) > 9:
|
||||||
@@ -250,7 +262,22 @@ class Table:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# ------------------------------------------------------------- substep
|
# ------------------------------------------------------------- substep
|
||||||
def _substep(self, ball, dt):
|
def _substep(self, ball, dt, n_sub=1):
|
||||||
|
# flippers: kinematic angles advance inside the substep so a fast sweep
|
||||||
|
# stays continuous w.r.t. the ball (no tunneling past a resting ball).
|
||||||
|
for f in self.flippers:
|
||||||
|
target = f["up"] if f["pressed"] else f["rest"]
|
||||||
|
rate = (FLIP_SNAP_UP if f["pressed"] else FLIP_SNAP_DOWN) * dt
|
||||||
|
d = target - f["angle"]
|
||||||
|
f["angle"] = target if abs(d) < rate else f["angle"] + (rate if d > 0 else -rate)
|
||||||
|
tip = flipper_tip(f)
|
||||||
|
fx, fy = f["prev_tip"]
|
||||||
|
# displacement this substep, scaled back to px/frame
|
||||||
|
f["tipvx"] = (tip[0] - fx) * n_sub
|
||||||
|
f["tipvy"] = (tip[1] - fy) * n_sub
|
||||||
|
f["prev_tip"] = tip
|
||||||
|
f["contacting"] = False
|
||||||
|
|
||||||
ball.vy += GRAVITY * dt
|
ball.vy += GRAVITY * dt
|
||||||
ball.vx *= (1.0 - 0.014 * dt)
|
ball.vx *= (1.0 - 0.014 * dt)
|
||||||
ball.vy *= (1.0 - 0.005 * dt)
|
ball.vy *= (1.0 - 0.005 * dt)
|
||||||
@@ -389,9 +416,7 @@ class Table:
|
|||||||
|
|
||||||
def _flipper_collide(self, ball, f):
|
def _flipper_collide(self, ball, f):
|
||||||
px, py = f["pivot"]
|
px, py = f["pivot"]
|
||||||
a = f["angle"]
|
tipx, tipy = flipper_tip(f)
|
||||||
tipx = px + math.cos(a) * f["length"]
|
|
||||||
tipy = py + math.sin(a) * f["length"]
|
|
||||||
t, cx, cy, d2 = seg_info(ball.x, ball.y, px, py, tipx, tipy)
|
t, cx, cy, d2 = seg_info(ball.x, ball.y, px, py, tipx, tipy)
|
||||||
rad = FLIP_RADIUS + ball.rad
|
rad = FLIP_RADIUS + ball.rad
|
||||||
if d2 >= rad * rad:
|
if d2 >= rad * rad:
|
||||||
@@ -415,14 +440,18 @@ class Table:
|
|||||||
self._flipper_impulse(ball, f, nx, ny)
|
self._flipper_impulse(ball, f, nx, ny)
|
||||||
|
|
||||||
def _flipper_impulse(self, ball, f, nx, ny):
|
def _flipper_impulse(self, ball, f, nx, ny):
|
||||||
"""Reflect relative velocity across the moving flipper surface."""
|
"""Reflect relative velocity across the moving flipper surface.
|
||||||
w = f["w"]
|
|
||||||
|
sv = the velocity of the flipper surface at the contact point. The tip
|
||||||
|
moves by (tipvx, tipvy) px/frame; points closer to the pivot move
|
||||||
|
proportionally less, so scale by the contact radius / length.
|
||||||
|
"""
|
||||||
px, py = f["pivot"]
|
px, py = f["pivot"]
|
||||||
# surface velocity of the flipper at the contact point, in px/frame
|
|
||||||
# rotating by angle w (screen-clockwise positive) => v = w * (-ry, rx)
|
|
||||||
rx, ry = ball.x - px, ball.y - py
|
rx, ry = ball.x - px, ball.y - py
|
||||||
svx = -ry * w
|
rc = math.hypot(rx, ry)
|
||||||
svy = rx * w
|
scale = 1.0 if f["length"] <= 0 else min(1.0, max(0.2, rc / f["length"]))
|
||||||
|
svx = f["tipvx"] * scale
|
||||||
|
svy = f["tipvy"] * scale
|
||||||
rvx = ball.vx - svx
|
rvx = ball.vx - svx
|
||||||
rvy = ball.vy - svy
|
rvy = ball.vy - svy
|
||||||
vn = rvx * nx + rvy * ny
|
vn = rvx * nx + rvy * ny
|
||||||
@@ -436,13 +465,10 @@ class Table:
|
|||||||
j = -vn # inelastic follow-through while pressing
|
j = -vn # inelastic follow-through while pressing
|
||||||
ball.vx += j * nx
|
ball.vx += j * nx
|
||||||
ball.vy += j * ny
|
ball.vy += j * ny
|
||||||
elif fresh and vn > 0.0:
|
# light tangential traction so resting balls ride the swipe a little
|
||||||
# tiny push to keep the ball from sinking into a resting flipper
|
|
||||||
pass
|
|
||||||
# tangential traction: transfer some of the flipper's swipe to the ball
|
|
||||||
tx, ty = -ny, nx
|
tx, ty = -ny, nx
|
||||||
vt = rvx * tx + rvy * ty
|
vt = rvx * tx + rvy * ty
|
||||||
traction = 0.35 if abs(w) > 0.01 else 0.0
|
traction = 0.18 if (abs(f["tipvx"]) + abs(f["tipvy"])) > 0.1 else 0.0
|
||||||
ball.vx += vt * tx * -traction + svx * 0.0
|
ball.vx -= traction * vt * tx
|
||||||
ball.vy += vt * ty * -traction
|
ball.vy -= traction * vt * ty
|
||||||
self._cap(ball)
|
self._cap(ball)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.3 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.0 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.0 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.3 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |