From 0760afa1489501d2bd2e5ec64249d6dff12b9c43 Mon Sep 17 00:00:00 2001 From: itzmarkoni Date: Tue, 5 May 2026 19:16:43 -0400 Subject: [PATCH] physics: real gravity+spring drop-in, friction-based spin deceleration --- programs/roulette.lua | 148 ++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 78 deletions(-) diff --git a/programs/roulette.lua b/programs/roulette.lua index 77d45b8..400fcee 100644 --- a/programs/roulette.lua +++ b/programs/roulette.lua @@ -28,12 +28,21 @@ end -- Constants / tunables ---------------------------------------------------------------------- -local POCKET_SIZE = 64 -- px per pocket cell (1 block) -local BALL_RADIUS = 16 -- px radius of the ball circle -local TRACK_INSET = 88 -- px from screen edge to ball centre track -local SPIN_TIME_MIN = 4 -- seconds -local SPIN_TIME_MAX = 7 -local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps target) +local POCKET_SIZE = 64 -- px per pocket cell (1 block) +local BALL_RADIUS = 16 -- px radius of the ball circle +local TRACK_INSET = 88 -- px from screen edge to ball centre track +local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps) + +-- Drop-in physics +local GRAVITY = 900 -- px/s^2 +local BOUNCE_DAMPING = 0.52 -- speed kept after each wall bounce +local SPRING_K = 6.0 -- spring constant pulling ball to target +local SPRING_DAMP = 2.8 -- spring damping coefficient + +-- Spin physics +local SPIN_SPEED_MIN = 18.0 -- pockets/sec initial angular speed +local SPIN_SPEED_MAX = 26.0 +local SPIN_FRICTION = 1.6 -- pockets/sec^2 deceleration local COL_RED = 0xE53935 local COL_BLACK = 0x212121 @@ -221,66 +230,57 @@ end ---------------------------------------------------------------------- -- Drop-in animation --- Ball falls from top-center, bounces off bottom, sides, settles into track +-- Ball spawns above screen center, falls under gravity, bounces off +-- track walls. After 2 bounces a spring takes over and homes it to +-- the target pocket position. ---------------------------------------------------------------------- -local GRAVITY = 800 -- px/s^2 -local BOUNCE_DAMPING = 0.55 -- velocity multiplier on bounce - local function dropInAnimation(targetX, targetY) - -- Start above screen centre - local bx = math.floor(PW / 2) - local by = -BALL_RADIUS - local vx = (targetX - bx) * 0.3 -- slight horizontal drift toward target - local vy = 0 + local bx = PW / 2 + local by = -BALL_RADIUS - 10 + local vx = (targetX - bx) * 0.25 + local vy = 80 - -- Bounce area: constrained to inner track region - local minX = TRACK_INSET - local maxX = PW - TRACK_INSET - local minY = TRACK_INSET - local maxY = PH - TRACK_INSET + local minX = TRACK_INSET - BALL_RADIUS + local maxX = PW - TRACK_INSET + BALL_RADIUS + local minY = TRACK_INSET - BALL_RADIUS + local maxY = PH - TRACK_INSET + BALL_RADIUS - local dt = FRAME_DELAY - local MAX_TIME = 3.0 -- seconds before we force-snap - local elapsed = 0 - - -- We'll gradually pull toward target after first bounce - local settled = false + local dt = FRAME_DELAY + local elapsed = 0 + local MAX_TIME = 4.0 + local bounces = 0 + local springing = false while elapsed < MAX_TIME do - vy = vy + GRAVITY * dt - bx = bx + vx * dt - by = by + vy * dt + if not springing then + vy = vy + GRAVITY * dt + bx = bx + vx * dt + by = by + vy * dt - -- Wall bounces - if bx < minX then - bx = minX; vx = math.abs(vx) * BOUNCE_DAMPING - elseif bx > maxX then - bx = maxX; vx = -math.abs(vx) * BOUNCE_DAMPING - end + local hit = false + if bx < minX then bx = minX; vx = math.abs(vx) * BOUNCE_DAMPING; hit = true end + if bx > maxX then bx = maxX; vx = -math.abs(vx) * BOUNCE_DAMPING; hit = true end + if by < minY then by = minY; vy = math.abs(vy) * BOUNCE_DAMPING; hit = true end + if by > maxY then by = maxY; vy = -math.abs(vy) * BOUNCE_DAMPING; hit = true end - if by < minY then - by = minY; vy = math.abs(vy) * BOUNCE_DAMPING - elseif by > maxY then - by = maxY; vy = -math.abs(vy) * BOUNCE_DAMPING - -- After first floor bounce, start homing toward target - settled = true - end - - -- Once settled, apply a soft spring toward target - if settled then + if hit then + bounces = bounces + 1 + if bounces >= 2 then springing = true end + end + else local dx = targetX - bx local dy = targetY - by - vx = vx + dx * 3 * dt - vy = vy + dy * 3 * dt - -- Dampen velocity - vx = vx * (1 - 2 * dt) - vy = vy * (1 - 2 * dt) - -- Close enough to snap - if math.abs(dx) < 3 and math.abs(dy) < 3 and - math.abs(vx) < 5 and math.abs(vy) < 5 then - break - end + vx = vx + dx * SPRING_K * dt + vy = vy + dy * SPRING_K * dt + vx = vx - vx * SPRING_DAMP * dt + vy = vy - vy * SPRING_DAMP * dt + bx = bx + vx * dt + by = by + vy * dt + + local speed = math.sqrt(vx*vx + vy*vy) + local dist = math.sqrt(dx*dx + dy*dy) + if dist < 2 and speed < 4 then break end end eraseBall(COL_TRACK) @@ -290,7 +290,6 @@ local function dropInAnimation(targetX, targetY) elapsed = elapsed + dt end - -- Snap exactly to target eraseBall(COL_TRACK) drawBallAt(targetX, targetY) gpu.sync() @@ -313,7 +312,9 @@ local function glowAnimation(pocketIdx) end ---------------------------------------------------------------------- --- Spin logic +-- Spin logic — friction-based physics +-- Ball starts with a random high angular speed (pockets/sec) and +-- decelerates under constant friction until it naturally stops. ---------------------------------------------------------------------- local function pocketPos(idx) @@ -327,28 +328,20 @@ local function lerpPos(i1, i2, t) return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t end -local function easeOut(t) - local inv = 1 - t - return 1 - inv * inv * inv -end - local currentPocketIdx = 1 local function spin() - local n = NUM_POCKETS - local spinTime = SPIN_TIME_MIN + math.random() * (SPIN_TIME_MAX - SPIN_TIME_MIN) - local laps = 4 + math.random(0, 3) - local finalIdx = math.random(1, n) - local startIdx = currentPocketIdx - local totalSteps = laps * n + ((finalIdx - startIdx) % n) - if totalSteps == 0 then totalSteps = n end + local n = NUM_POCKETS + local speed = SPIN_SPEED_MIN + math.random() * (SPIN_SPEED_MAX - SPIN_SPEED_MIN) + local posF = currentPocketIdx - 1 -- fractional pocket index (0-based) + local dt = FRAME_DELAY - local elapsed = 0 + while speed > 0 do + speed = speed - SPIN_FRICTION * dt + if speed < 0 then speed = 0 end + + posF = (posF + speed * dt) % n - while elapsed < spinTime do - local t = math.min(elapsed / spinTime, 1) - local eased = easeOut(t) - local posF = (startIdx - 1 + eased * totalSteps) % n local idxLow = math.floor(posF) % n + 1 local idxHi = idxLow % n + 1 local frac = posF - math.floor(posF) @@ -357,12 +350,11 @@ local function spin() local bx, by = lerpPos(idxLow, idxHi, frac) drawBallAt(bx, by) gpu.sync() - - sleep(FRAME_DELAY) - elapsed = elapsed + FRAME_DELAY + sleep(dt) end - -- Snap to final pocket + -- Snap to nearest pocket + local finalIdx = math.floor(posF + 0.5) % n + 1 eraseBall(COL_TRACK) local fx, fy = pocketPos(finalIdx) drawBallAt(fx, fy)