diff --git a/game.py b/game.py index 516a68e..27e021c 100644 --- a/game.py +++ b/game.py @@ -9,7 +9,7 @@ import pygame 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, 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 # --------------------------------------------------------------------------- @@ -195,9 +195,7 @@ def draw_sling(surf, s): def draw_flipper(surf, f, side): px, py = f["pivot"] - a = f["angle"] - tipx = px + math.cos(a) * f["length"] - tipy = py + math.sin(a) * f["length"] + tipx, tipy = flipper_tip(f) p0 = (int(px), int(py)) p1 = (int(tipx), int(tipy)) # 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"], (ex, ey), 4) # 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) tx, ty = int(tipx + nx * 3), int(tipy + ny * 3) pygame.draw.line(surf, C["flipper_l"], (hx, hy), (tx, ty), 2) diff --git a/physics.py b/physics.py index 8ea44a2..4a927ba 100644 --- a/physics.py +++ b/physics.py @@ -23,7 +23,7 @@ FLIP_UP = -math.radians(52.0) # fired position FLIP_PIVOT_L = (62.0, 338.0) FLIP_PIVOT_R = (418.0, 338.0) 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_BOUNCE = 0.12 @@ -72,6 +72,20 @@ def seg_info(px, py, ax, ay, bx, by): 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: __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, 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.ball = Ball(CANNON[0], CANNON[1]) @@ -204,20 +221,15 @@ class Table: ball = self.ball - # flipper angle update (kinematic; per-frame, then substepped) - 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 - 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 - + # flippers move inside substeps; sweep 6 steps/frame while pressed so + # the paddle never jumps over a resting ball + flips_moving = any(f["pressed"] for f in self.flippers) 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): - self._substep(ball, 1.0 / n_sub) + self._substep(ball, 1.0 / n_sub, n_sub) self.trail.append((ball.x, ball.y)) if len(self.trail) > 9: @@ -250,7 +262,22 @@ class Table: return False # ------------------------------------------------------------- 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.vx *= (1.0 - 0.014 * dt) ball.vy *= (1.0 - 0.005 * dt) @@ -389,9 +416,7 @@ class Table: def _flipper_collide(self, ball, f): px, py = f["pivot"] - a = f["angle"] - tipx = px + math.cos(a) * f["length"] - tipy = py + math.sin(a) * f["length"] + tipx, tipy = flipper_tip(f) t, cx, cy, d2 = seg_info(ball.x, ball.y, px, py, tipx, tipy) rad = FLIP_RADIUS + ball.rad if d2 >= rad * rad: @@ -415,14 +440,18 @@ class Table: self._flipper_impulse(ball, f, nx, ny) def _flipper_impulse(self, ball, f, nx, ny): - """Reflect relative velocity across the moving flipper surface.""" - w = f["w"] + """Reflect relative velocity across the moving flipper surface. + + 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"] - # 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 - svx = -ry * w - svy = rx * w + rc = math.hypot(rx, ry) + 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 rvy = ball.vy - svy vn = rvx * nx + rvy * ny @@ -436,13 +465,10 @@ class Table: j = -vn # inelastic follow-through while pressing ball.vx += j * nx ball.vy += j * ny - elif fresh and vn > 0.0: - # 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 + # light tangential traction so resting balls ride the swipe a little tx, ty = -ny, nx vt = rvx * tx + rvy * ty - traction = 0.35 if abs(w) > 0.01 else 0.0 - ball.vx += vt * tx * -traction + svx * 0.0 - ball.vy += vt * ty * -traction + traction = 0.18 if (abs(f["tipvx"]) + abs(f["tipvy"])) > 0.1 else 0.0 + ball.vx -= traction * vt * tx + ball.vy -= traction * vt * ty self._cap(ball) diff --git a/shots/1_title.png b/shots/1_title.png index a8b96cb..5d24fd8 100644 Binary files a/shots/1_title.png and b/shots/1_title.png differ diff --git a/shots/1_title_2x.png b/shots/1_title_2x.png index 0bef325..cc61d25 100644 Binary files a/shots/1_title_2x.png and b/shots/1_title_2x.png differ diff --git a/shots/2_serve.png b/shots/2_serve.png index fe6d12c..292e6f5 100644 Binary files a/shots/2_serve.png and b/shots/2_serve.png differ diff --git a/shots/2_serve_2x.png b/shots/2_serve_2x.png index 2a0a536..b91af54 100644 Binary files a/shots/2_serve_2x.png and b/shots/2_serve_2x.png differ diff --git a/shots/3_play.png b/shots/3_play.png index 5242145..0ac7f61 100644 Binary files a/shots/3_play.png and b/shots/3_play.png differ diff --git a/shots/3_play_2x.png b/shots/3_play_2x.png index b41b23e..a3f8377 100644 Binary files a/shots/3_play_2x.png and b/shots/3_play_2x.png differ diff --git a/shots/4_fever.png b/shots/4_fever.png index 5c595b2..8b037d3 100644 Binary files a/shots/4_fever.png and b/shots/4_fever.png differ diff --git a/shots/4_fever_2x.png b/shots/4_fever_2x.png index 94d0fb7..c4edd41 100644 Binary files a/shots/4_fever_2x.png and b/shots/4_fever_2x.png differ diff --git a/shots/5_drain.png b/shots/5_drain.png index 4f00805..885e54c 100644 Binary files a/shots/5_drain.png and b/shots/5_drain.png differ diff --git a/shots/5_drain_2x.png b/shots/5_drain_2x.png index 74385c9..0d70199 100644 Binary files a/shots/5_drain_2x.png and b/shots/5_drain_2x.png differ diff --git a/shots/6_gameover.png b/shots/6_gameover.png index 5cc14e3..93b55a6 100644 Binary files a/shots/6_gameover.png and b/shots/6_gameover.png differ diff --git a/shots/6_gameover_2x.png b/shots/6_gameover_2x.png index aa15109..af72a2f 100644 Binary files a/shots/6_gameover_2x.png and b/shots/6_gameover_2x.png differ