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 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 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 MAX_TIME = 4.0
|
||||
local bounces = 0
|
||||
local springing = false
|
||||
|
||||
while elapsed < MAX_TIME do
|
||||
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
|
||||
if hit then
|
||||
bounces = bounces + 1
|
||||
if bounces >= 2 then springing = true end
|
||||
end
|
||||
|
||||
-- Once settled, apply a soft spring toward target
|
||||
if settled then
|
||||
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 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)
|
||||
|
||||
Reference in New Issue
Block a user