physics: real gravity+spring drop-in, friction-based spin deceleration

This commit is contained in:
2026-05-05 19:16:43 -04:00
parent 4e90d405c5
commit 0760afa148

View File

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