physics: real gravity+spring drop-in, friction-based spin deceleration
This commit is contained in:
@@ -31,9 +31,18 @@ end
|
|||||||
local POCKET_SIZE = 64 -- px per pocket cell (1 block)
|
local POCKET_SIZE = 64 -- px per pocket cell (1 block)
|
||||||
local BALL_RADIUS = 16 -- px radius of the ball circle
|
local BALL_RADIUS = 16 -- px radius of the ball circle
|
||||||
local TRACK_INSET = 88 -- px from screen edge to ball centre track
|
local TRACK_INSET = 88 -- px from screen edge to ball centre track
|
||||||
local SPIN_TIME_MIN = 4 -- seconds
|
local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps)
|
||||||
local SPIN_TIME_MAX = 7
|
|
||||||
local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps target)
|
-- 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_RED = 0xE53935
|
||||||
local COL_BLACK = 0x212121
|
local COL_BLACK = 0x212121
|
||||||
@@ -221,66 +230,57 @@ end
|
|||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Drop-in animation
|
-- 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)
|
local function dropInAnimation(targetX, targetY)
|
||||||
-- Start above screen centre
|
local bx = PW / 2
|
||||||
local bx = math.floor(PW / 2)
|
local by = -BALL_RADIUS - 10
|
||||||
local by = -BALL_RADIUS
|
local vx = (targetX - bx) * 0.25
|
||||||
local vx = (targetX - bx) * 0.3 -- slight horizontal drift toward target
|
local vy = 80
|
||||||
local vy = 0
|
|
||||||
|
|
||||||
-- Bounce area: constrained to inner track region
|
local minX = TRACK_INSET - BALL_RADIUS
|
||||||
local minX = TRACK_INSET
|
local maxX = PW - TRACK_INSET + BALL_RADIUS
|
||||||
local maxX = PW - TRACK_INSET
|
local minY = TRACK_INSET - BALL_RADIUS
|
||||||
local minY = TRACK_INSET
|
local maxY = PH - TRACK_INSET + BALL_RADIUS
|
||||||
local maxY = PH - TRACK_INSET
|
|
||||||
|
|
||||||
local dt = FRAME_DELAY
|
local dt = FRAME_DELAY
|
||||||
local MAX_TIME = 3.0 -- seconds before we force-snap
|
|
||||||
local elapsed = 0
|
local elapsed = 0
|
||||||
|
local MAX_TIME = 4.0
|
||||||
-- We'll gradually pull toward target after first bounce
|
local bounces = 0
|
||||||
local settled = false
|
local springing = false
|
||||||
|
|
||||||
while elapsed < MAX_TIME do
|
while elapsed < MAX_TIME do
|
||||||
|
if not springing then
|
||||||
vy = vy + GRAVITY * dt
|
vy = vy + GRAVITY * dt
|
||||||
bx = bx + vx * dt
|
bx = bx + vx * dt
|
||||||
by = by + vy * dt
|
by = by + vy * dt
|
||||||
|
|
||||||
-- Wall bounces
|
local hit = false
|
||||||
if bx < minX then
|
if bx < minX then bx = minX; vx = math.abs(vx) * BOUNCE_DAMPING; hit = true end
|
||||||
bx = minX; vx = math.abs(vx) * BOUNCE_DAMPING
|
if bx > maxX then bx = maxX; vx = -math.abs(vx) * BOUNCE_DAMPING; hit = true end
|
||||||
elseif bx > maxX then
|
if by < minY then by = minY; vy = math.abs(vy) * BOUNCE_DAMPING; hit = true end
|
||||||
bx = maxX; vx = -math.abs(vx) * BOUNCE_DAMPING
|
if by > maxY then by = maxY; vy = -math.abs(vy) * BOUNCE_DAMPING; hit = true end
|
||||||
end
|
|
||||||
|
|
||||||
if by < minY then
|
if hit then
|
||||||
by = minY; vy = math.abs(vy) * BOUNCE_DAMPING
|
bounces = bounces + 1
|
||||||
elseif by > maxY then
|
if bounces >= 2 then springing = true end
|
||||||
by = maxY; vy = -math.abs(vy) * BOUNCE_DAMPING
|
|
||||||
-- After first floor bounce, start homing toward target
|
|
||||||
settled = true
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
-- Once settled, apply a soft spring toward target
|
|
||||||
if settled then
|
|
||||||
local dx = targetX - bx
|
local dx = targetX - bx
|
||||||
local dy = targetY - by
|
local dy = targetY - by
|
||||||
vx = vx + dx * 3 * dt
|
vx = vx + dx * SPRING_K * dt
|
||||||
vy = vy + dy * 3 * dt
|
vy = vy + dy * SPRING_K * dt
|
||||||
-- Dampen velocity
|
vx = vx - vx * SPRING_DAMP * dt
|
||||||
vx = vx * (1 - 2 * dt)
|
vy = vy - vy * SPRING_DAMP * dt
|
||||||
vy = vy * (1 - 2 * dt)
|
bx = bx + vx * dt
|
||||||
-- Close enough to snap
|
by = by + vy * dt
|
||||||
if math.abs(dx) < 3 and math.abs(dy) < 3 and
|
|
||||||
math.abs(vx) < 5 and math.abs(vy) < 5 then
|
local speed = math.sqrt(vx*vx + vy*vy)
|
||||||
break
|
local dist = math.sqrt(dx*dx + dy*dy)
|
||||||
end
|
if dist < 2 and speed < 4 then break end
|
||||||
end
|
end
|
||||||
|
|
||||||
eraseBall(COL_TRACK)
|
eraseBall(COL_TRACK)
|
||||||
@@ -290,7 +290,6 @@ local function dropInAnimation(targetX, targetY)
|
|||||||
elapsed = elapsed + dt
|
elapsed = elapsed + dt
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Snap exactly to target
|
|
||||||
eraseBall(COL_TRACK)
|
eraseBall(COL_TRACK)
|
||||||
drawBallAt(targetX, targetY)
|
drawBallAt(targetX, targetY)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
@@ -313,7 +312,9 @@ local function glowAnimation(pocketIdx)
|
|||||||
end
|
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)
|
local function pocketPos(idx)
|
||||||
@@ -327,28 +328,20 @@ local function lerpPos(i1, i2, t)
|
|||||||
return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t
|
return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t
|
||||||
end
|
end
|
||||||
|
|
||||||
local function easeOut(t)
|
|
||||||
local inv = 1 - t
|
|
||||||
return 1 - inv * inv * inv
|
|
||||||
end
|
|
||||||
|
|
||||||
local currentPocketIdx = 1
|
local currentPocketIdx = 1
|
||||||
|
|
||||||
local function spin()
|
local function spin()
|
||||||
local n = NUM_POCKETS
|
local n = NUM_POCKETS
|
||||||
local spinTime = SPIN_TIME_MIN + math.random() * (SPIN_TIME_MAX - SPIN_TIME_MIN)
|
local speed = SPIN_SPEED_MIN + math.random() * (SPIN_SPEED_MAX - SPIN_SPEED_MIN)
|
||||||
local laps = 4 + math.random(0, 3)
|
local posF = currentPocketIdx - 1 -- fractional pocket index (0-based)
|
||||||
local finalIdx = math.random(1, n)
|
local dt = FRAME_DELAY
|
||||||
local startIdx = currentPocketIdx
|
|
||||||
local totalSteps = laps * n + ((finalIdx - startIdx) % n)
|
|
||||||
if totalSteps == 0 then totalSteps = n end
|
|
||||||
|
|
||||||
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 idxLow = math.floor(posF) % n + 1
|
||||||
local idxHi = idxLow % n + 1
|
local idxHi = idxLow % n + 1
|
||||||
local frac = posF - math.floor(posF)
|
local frac = posF - math.floor(posF)
|
||||||
@@ -357,12 +350,11 @@ local function spin()
|
|||||||
local bx, by = lerpPos(idxLow, idxHi, frac)
|
local bx, by = lerpPos(idxLow, idxHi, frac)
|
||||||
drawBallAt(bx, by)
|
drawBallAt(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
sleep(dt)
|
||||||
sleep(FRAME_DELAY)
|
|
||||||
elapsed = elapsed + FRAME_DELAY
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Snap to final pocket
|
-- Snap to nearest pocket
|
||||||
|
local finalIdx = math.floor(posF + 0.5) % n + 1
|
||||||
eraseBall(COL_TRACK)
|
eraseBall(COL_TRACK)
|
||||||
local fx, fy = pocketPos(finalIdx)
|
local fx, fy = pocketPos(finalIdx)
|
||||||
drawBallAt(fx, fy)
|
drawBallAt(fx, fy)
|
||||||
|
|||||||
Reference in New Issue
Block a user