updated
This commit is contained in:
473
programs/prizewheel.lua
Normal file
473
programs/prizewheel.lua
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
-- Prize Wheel — spinning prize wheel, perspective view
|
||||||
|
-- Tom's Peripherals GPU + screen wall.
|
||||||
|
--
|
||||||
|
-- The wheel is drawn as a perspective ellipse (top tilted away from viewer).
|
||||||
|
-- It physically rotates: angular velocity starts high and decays under
|
||||||
|
-- friction until it stops. A fixed pointer at the top selects the prize.
|
||||||
|
--
|
||||||
|
-- Rendering approach:
|
||||||
|
-- A point at wheel angle `a`, radius fraction `f` maps to screen as:
|
||||||
|
-- sx = CX + f * RX * cos(a + wheelAngle)
|
||||||
|
-- sy = WY + f * RY * sin(a + wheelAngle) (RY = RX * TILT)
|
||||||
|
-- Wedges are rasterised row-by-row using ellipse span math.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- GPU discovery
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function findGPU()
|
||||||
|
print("[prizewheel] Scanning peripherals...")
|
||||||
|
for _, name in ipairs(peripheral.getNames()) do
|
||||||
|
local t = peripheral.getType(name)
|
||||||
|
print(" " .. name .. " = " .. tostring(t))
|
||||||
|
if t and t:find("gpu") then
|
||||||
|
print("[prizewheel] Using GPU: " .. name)
|
||||||
|
return peripheral.wrap(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Constants
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local FRAME_DELAY = 0.033 -- ~30 fps
|
||||||
|
local TWO_PI = math.pi * 2
|
||||||
|
|
||||||
|
-- Perspective tilt: RY / RX. 0 = edge-on, 1 = top-down.
|
||||||
|
-- 0.28 ≈ wheel tilted ~74° toward viewer (like a real prize wheel on a stand).
|
||||||
|
local TILT = 0.28
|
||||||
|
|
||||||
|
-- Prize segments — name, colour, relative weight (wider = more likely)
|
||||||
|
local PRIZES = {
|
||||||
|
{ name = "$100", col = 0xF44336, weight = 2 }, -- red
|
||||||
|
{ name = "SPIN AGAIN", col = 0xFFEB3B, weight = 3 }, -- yellow
|
||||||
|
{ name = "$500", col = 0x4CAF50, weight = 1 }, -- green
|
||||||
|
{ name = "BANKRUPT", col = 0x212121, weight = 2 }, -- near-black
|
||||||
|
{ name = "$250", col = 0x2196F3, weight = 2 }, -- blue
|
||||||
|
{ name = "$50", col = 0xFF9800, weight = 3 }, -- orange
|
||||||
|
{ name = "JACKPOT", col = 0xE91E63, weight = 1 }, -- pink
|
||||||
|
{ name = "$150", col = 0x9C27B0, weight = 2 }, -- purple
|
||||||
|
{ name = "LOSE TURN", col = 0x607D8B, weight = 2 }, -- grey-blue
|
||||||
|
{ name = "$75", col = 0x00BCD4, weight = 3 }, -- cyan
|
||||||
|
{ name = "$1000", col = 0x8BC34A, weight = 1 }, -- lime
|
||||||
|
{ name = "$25", col = 0xFF5722, weight = 3 }, -- deep orange
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Build cumulative angle table from weights
|
||||||
|
local function buildSegments()
|
||||||
|
local total = 0
|
||||||
|
for _, p in ipairs(PRIZES) do total = total + p.weight end
|
||||||
|
local segs = {}
|
||||||
|
local cumAngle = 0
|
||||||
|
for i, p in ipairs(PRIZES) do
|
||||||
|
local arc = (p.weight / total) * TWO_PI
|
||||||
|
segs[i] = {
|
||||||
|
name = p.name,
|
||||||
|
col = p.col,
|
||||||
|
startA = cumAngle,
|
||||||
|
endA = cumAngle + arc,
|
||||||
|
midA = cumAngle + arc / 2,
|
||||||
|
}
|
||||||
|
cumAngle = cumAngle + arc
|
||||||
|
end
|
||||||
|
return segs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Physics spin constants
|
||||||
|
local OMEGA_MIN = 4.0 -- rad/s minimum starting spin
|
||||||
|
local OMEGA_MAX = 10.0 -- rad/s maximum starting spin
|
||||||
|
local FRICTION = 0.987 -- angular velocity multiplier per frame
|
||||||
|
local STOP_OMEGA = 0.04 -- rad/s below this we consider it stopped
|
||||||
|
|
||||||
|
-- Colours
|
||||||
|
local COL_BG = 0x050505
|
||||||
|
local COL_RIM = 0x8B6914 -- gold rim
|
||||||
|
local COL_RIM_DARK = 0x5C4400
|
||||||
|
local COL_SPOKE = 0xB8860B
|
||||||
|
local COL_HUB = 0x333333
|
||||||
|
local COL_HUB_RING = 0x8B6914
|
||||||
|
local COL_SEP = 0xFFFFFF -- wedge separator lines
|
||||||
|
local COL_STAND = 0x5D4037 -- wood brown
|
||||||
|
local COL_STAND_DRK = 0x3E2723
|
||||||
|
local COL_POINTER = 0xF5F5F5
|
||||||
|
local COL_POINTER_S = 0x444444
|
||||||
|
local COL_WHITE = 0xFFFFFF
|
||||||
|
local COL_GLOW = 0xFFD600 -- winner highlight
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- GPU / pixel primitives
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local gpu
|
||||||
|
local PW, PH
|
||||||
|
|
||||||
|
-- Screen geometry (set in start())
|
||||||
|
local CX, WY -- wheel centre x, wheel centre y
|
||||||
|
local RX, RY -- wheel half-width, half-height (perspective)
|
||||||
|
|
||||||
|
local function px_rect(x, y, w, h, col)
|
||||||
|
x = math.floor(x); y = math.floor(y)
|
||||||
|
w = math.floor(w); h = math.floor(h)
|
||||||
|
if x < 1 then w = w + x - 1; x = 1 end
|
||||||
|
if y < 1 then h = h + y - 1; y = 1 end
|
||||||
|
if x + w - 1 > PW then w = PW - x + 1 end
|
||||||
|
if y + h - 1 > PH then h = PH - y + 1 end
|
||||||
|
if w < 1 or h < 1 then return end
|
||||||
|
gpu.filledRectangle(x, y, w, h, col)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function px_text(str, x, y, fg, bg, size)
|
||||||
|
pcall(gpu.drawText, math.floor(x), math.floor(y), str, fg, bg, size or 1, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function px_text_centre(str, y, fg, bg, size)
|
||||||
|
size = size or 1
|
||||||
|
local w = #str * 6 * size
|
||||||
|
px_text(str, CX - math.floor(w / 2), y, fg, bg, size)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Filled ellipse
|
||||||
|
local function px_ellipse(cx, cy, rx, ry, col)
|
||||||
|
cx = math.floor(cx); cy = math.floor(cy)
|
||||||
|
rx = math.floor(rx); ry = math.floor(ry)
|
||||||
|
if rx < 1 or ry < 1 then return end
|
||||||
|
for dy = -ry, ry do
|
||||||
|
local t = dy / ry
|
||||||
|
local half = math.floor(rx * math.sqrt(math.max(0, 1 - t*t)) + 0.5)
|
||||||
|
if half >= 1 then
|
||||||
|
px_rect(cx - half, cy + dy, half * 2 + 1, 1, col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ellipse annulus
|
||||||
|
local function px_ellipse_annulus(cx, cy, rx1, ry1, rx2, ry2, col)
|
||||||
|
cx = math.floor(cx); cy = math.floor(cy)
|
||||||
|
for dy = -ry2, ry2 do
|
||||||
|
local t2 = dy / ry2
|
||||||
|
local ho = math.floor(rx2 * math.sqrt(math.max(0, 1 - t2*t2)) + 0.5)
|
||||||
|
local hi = 0
|
||||||
|
if math.abs(dy) <= ry1 then
|
||||||
|
local t1 = dy / ry1
|
||||||
|
hi = math.floor(rx1 * math.sqrt(math.max(0, 1 - t1*t1)) + 0.5)
|
||||||
|
end
|
||||||
|
if ho > hi then
|
||||||
|
px_rect(cx - ho, cy + dy, ho - hi, 1, col)
|
||||||
|
px_rect(cx + hi, cy + dy, ho - hi + 1, 1, col)
|
||||||
|
elseif hi == 0 then
|
||||||
|
px_rect(cx - ho, cy + dy, ho*2 + 1, 1, col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Wheel drawing
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local segments = {}
|
||||||
|
|
||||||
|
-- Convert wheel-local polar (angle, radius fraction) → screen (sx, sy)
|
||||||
|
-- wheelAngle is the current rotation offset
|
||||||
|
local function wheelToScreen(a, f, wheelAngle)
|
||||||
|
local wa = a + wheelAngle
|
||||||
|
return CX + f * RX * math.cos(wa),
|
||||||
|
WY + f * RY * math.sin(wa)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw a single wedge of the perspective ellipse.
|
||||||
|
-- seg.startA / seg.endA are in wheel-local angles.
|
||||||
|
-- wheelAngle rotates the whole wheel.
|
||||||
|
local function drawWedge(seg, wheelAngle, glowing)
|
||||||
|
local col = seg.col
|
||||||
|
if glowing then
|
||||||
|
local r = math.min(255, math.floor(col / 0x10000) + 80)
|
||||||
|
local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 80)
|
||||||
|
local b = math.min(255, col % 0x100 + 80)
|
||||||
|
col = r * 0x10000 + g * 0x100 + b
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rasterise the wedge by scanning screen rows within the ellipse bounding box.
|
||||||
|
local by0 = math.floor(WY - RY) - 1
|
||||||
|
local by1 = math.ceil (WY + RY) + 1
|
||||||
|
local bx0 = math.floor(CX - RX) - 1
|
||||||
|
local bx1 = math.ceil (CX + RX) + 1
|
||||||
|
|
||||||
|
local a0 = seg.startA + wheelAngle
|
||||||
|
local a1 = seg.endA + wheelAngle
|
||||||
|
local arc = seg.endA - seg.startA -- always positive
|
||||||
|
|
||||||
|
for sy = by0, by1 do
|
||||||
|
local dy = sy - WY
|
||||||
|
-- ellipse x half-span at this row
|
||||||
|
if math.abs(dy) <= RY then
|
||||||
|
local t = dy / RY
|
||||||
|
local xhalf = RX * math.sqrt(math.max(0, 1 - t*t))
|
||||||
|
local runStart = nil
|
||||||
|
for sx = math.floor(CX - xhalf), math.ceil(CX + xhalf) do
|
||||||
|
local dx = sx - CX
|
||||||
|
local angle = math.atan2(dy / RY, dx / RX) -- ellipse-normalised angle
|
||||||
|
local rel = (angle - a0) % TWO_PI
|
||||||
|
if rel <= arc then
|
||||||
|
if not runStart then runStart = sx end
|
||||||
|
else
|
||||||
|
if runStart then
|
||||||
|
px_rect(runStart, sy, sx - runStart, 1, col)
|
||||||
|
runStart = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if runStart then
|
||||||
|
px_rect(runStart, sy, math.ceil(CX + xhalf) - runStart + 1, 1, col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Separator line at startA edge
|
||||||
|
local steps = math.floor(RX)
|
||||||
|
for i = 0, steps do
|
||||||
|
local f = i / steps
|
||||||
|
local sx = CX + f * RX * math.cos(a0)
|
||||||
|
local sy = WY + f * RY * math.sin(a0)
|
||||||
|
px_rect(math.floor(sx), math.floor(sy), 2, 1, COL_SEP)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Label at wedge midpoint, ~70% radius
|
||||||
|
local midA = seg.midA + wheelAngle
|
||||||
|
local lx = CX + 0.70 * RX * math.cos(midA)
|
||||||
|
local ly = WY + 0.70 * RY * math.sin(midA)
|
||||||
|
local label = seg.name
|
||||||
|
local lsize = (RX > 80) and 1 or 1
|
||||||
|
px_text(label, lx - math.floor(#label * 3), ly - 4, COL_WHITE, col, lsize)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawAllWedges(wheelAngle, glowIdx)
|
||||||
|
for i, seg in ipairs(segments) do
|
||||||
|
drawWedge(seg, wheelAngle, i == glowIdx)
|
||||||
|
sleep(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawChrome(wheelAngle)
|
||||||
|
-- Outer rim (ellipse annulus, slightly larger than wheel)
|
||||||
|
local rimRX = RX + 6; local rimRY = RY + math.floor(6 * TILT)
|
||||||
|
px_ellipse_annulus(CX, WY, RX, RY, rimRX, rimRY, COL_RIM)
|
||||||
|
-- Inner hub
|
||||||
|
local hubRX = math.floor(RX * 0.10)
|
||||||
|
local hubRY = math.floor(RY * 0.10)
|
||||||
|
px_ellipse(CX, WY, hubRX + 3, hubRY + 3, COL_HUB_RING)
|
||||||
|
px_ellipse(CX, WY, hubRX, hubRY, COL_HUB)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawStand()
|
||||||
|
-- Two angled legs below the wheel
|
||||||
|
local baseY = WY + RY + 6
|
||||||
|
local legBot = PH - 4
|
||||||
|
local legW = 8
|
||||||
|
-- Left leg
|
||||||
|
local lx1 = CX - math.floor(RX * 0.3)
|
||||||
|
local lx2 = CX - math.floor(RX * 0.7)
|
||||||
|
-- Draw as a trapezoid approximation with filled rects
|
||||||
|
local steps = legBot - baseY
|
||||||
|
for i = 0, steps do
|
||||||
|
local frac = i / math.max(1, steps)
|
||||||
|
local cx_l = lx1 + math.floor((lx2 - lx1) * frac)
|
||||||
|
px_rect(cx_l - legW//2, baseY + i, legW, 1, COL_STAND)
|
||||||
|
end
|
||||||
|
-- Right leg
|
||||||
|
local rx1 = CX + math.floor(RX * 0.3)
|
||||||
|
local rx2 = CX + math.floor(RX * 0.7)
|
||||||
|
for i = 0, steps do
|
||||||
|
local frac = i / math.max(1, steps)
|
||||||
|
local cx_r = rx1 + math.floor((rx2 - rx1) * frac)
|
||||||
|
px_rect(cx_r - legW//2, baseY + i, legW, 1, COL_STAND)
|
||||||
|
end
|
||||||
|
-- Horizontal crossbar
|
||||||
|
local barY = baseY + math.floor(steps * 0.55)
|
||||||
|
px_rect(lx2 - legW//2, barY, rx2 - lx2 + legW, 6, COL_STAND_DRK)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fixed pointer triangle at the top of the wheel (screen top, pointing down)
|
||||||
|
local function drawPointer()
|
||||||
|
local tipX = CX
|
||||||
|
local tipY = WY - RY - 5 -- just above the rim
|
||||||
|
local baseY = tipY - 18
|
||||||
|
local halfW = 10
|
||||||
|
-- Shadow
|
||||||
|
for i = 0, 18 do
|
||||||
|
local frac = i / 18
|
||||||
|
local hw = math.floor(halfW * (1 - frac)) + 1
|
||||||
|
px_rect(tipX - hw + 2, baseY + i + 2, hw*2, 1, COL_POINTER_S)
|
||||||
|
end
|
||||||
|
-- Arrow
|
||||||
|
for i = 0, 18 do
|
||||||
|
local frac = i / 18
|
||||||
|
local hw = math.floor(halfW * (1 - frac)) + 1
|
||||||
|
px_rect(tipX - hw, baseY + i, hw*2, 1, COL_POINTER)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawWheelFull(wheelAngle, glowIdx)
|
||||||
|
-- Clear wheel area
|
||||||
|
local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT)
|
||||||
|
px_ellipse(CX, WY, rimRX, rimRY, COL_BG)
|
||||||
|
drawAllWedges(wheelAngle, glowIdx)
|
||||||
|
drawChrome(wheelAngle)
|
||||||
|
drawPointer()
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Center / result text (drawn below wheel, above stand)
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function drawResultText(lines)
|
||||||
|
local y0 = WY + RY + math.floor(RY * 0.15)
|
||||||
|
-- Clear area
|
||||||
|
px_rect(1, y0, PW, 30, COL_BG)
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
px_text_centre(line, y0 + (i-1) * 14, COL_WHITE, COL_BG, 1)
|
||||||
|
end
|
||||||
|
gpu.sync()
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Spin physics
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Find which segment index is currently under the pointer.
|
||||||
|
-- The pointer is at the top of the wheel = screen angle -pi/2.
|
||||||
|
-- In wheel-local space that is angle (-pi/2 - wheelAngle) mod TWO_PI.
|
||||||
|
local function segmentAtPointer(wheelAngle)
|
||||||
|
local pointerLocalAngle = (-math.pi / 2 - wheelAngle) % TWO_PI
|
||||||
|
for i, seg in ipairs(segments) do
|
||||||
|
if pointerLocalAngle >= seg.startA and pointerLocalAngle < seg.endA then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function spin()
|
||||||
|
local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN)
|
||||||
|
local angle = math.random() * TWO_PI -- random start rotation
|
||||||
|
local elapsed = 0
|
||||||
|
local MAX_TIME = 30.0
|
||||||
|
|
||||||
|
-- Draw wheel at initial angle
|
||||||
|
drawWheelFull(angle, nil)
|
||||||
|
gpu.sync()
|
||||||
|
|
||||||
|
while elapsed < MAX_TIME do
|
||||||
|
-- Decay angular velocity
|
||||||
|
omega = omega * FRICTION
|
||||||
|
|
||||||
|
-- Integrate angle
|
||||||
|
angle = angle + omega * FRAME_DELAY
|
||||||
|
-- Keep angle in [0, TWO_PI) to avoid float drift over long spins
|
||||||
|
if angle > TWO_PI * 100 then angle = angle % TWO_PI end
|
||||||
|
|
||||||
|
-- Erase wheel, redraw at new angle
|
||||||
|
local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT)
|
||||||
|
px_ellipse(CX, WY, rimRX, rimRY, COL_BG)
|
||||||
|
drawAllWedges(angle, nil)
|
||||||
|
drawChrome(angle)
|
||||||
|
drawPointer()
|
||||||
|
gpu.sync()
|
||||||
|
|
||||||
|
sleep(FRAME_DELAY)
|
||||||
|
elapsed = elapsed + FRAME_DELAY
|
||||||
|
|
||||||
|
if omega < STOP_OMEGA then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Final angle
|
||||||
|
local winIdx = segmentAtPointer(angle)
|
||||||
|
|
||||||
|
-- Glow animation
|
||||||
|
for flash = 1, 7 do
|
||||||
|
local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT)
|
||||||
|
px_ellipse(CX, WY, rimRX, rimRY, COL_BG)
|
||||||
|
drawAllWedges(angle, flash % 2 == 1 and winIdx or nil)
|
||||||
|
drawChrome(angle)
|
||||||
|
drawPointer()
|
||||||
|
gpu.sync()
|
||||||
|
sleep(0.14)
|
||||||
|
end
|
||||||
|
|
||||||
|
return winIdx
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Redstone helper
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function waitForRedstonePulse()
|
||||||
|
while true do
|
||||||
|
os.pullEvent("redstone")
|
||||||
|
for _, side in ipairs(redstone.getSides()) do
|
||||||
|
if redstone.getInput(side) then return end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Lifecycle
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function start()
|
||||||
|
math.randomseed(os.epoch("utc"))
|
||||||
|
|
||||||
|
gpu = findGPU()
|
||||||
|
if not gpu then error("No GPU peripheral found.") end
|
||||||
|
|
||||||
|
gpu.refreshSize()
|
||||||
|
sleep(0)
|
||||||
|
gpu.setSize(64)
|
||||||
|
|
||||||
|
PW, PH = gpu.getSize()
|
||||||
|
print(("[prizewheel] GPU: %dx%d px"):format(PW, PH))
|
||||||
|
if not PW or PW < 128 or PH < 128 then
|
||||||
|
error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Geometry: wheel sits in the upper ~60% of the screen
|
||||||
|
CX = math.floor(PW / 2)
|
||||||
|
RX = math.floor(math.min(PW, PH) / 2) - 12
|
||||||
|
RY = math.floor(RX * TILT)
|
||||||
|
WY = math.floor(PH * 0.38) -- wheel centre sits above mid
|
||||||
|
|
||||||
|
segments = buildSegments()
|
||||||
|
|
||||||
|
gpu.fill(COL_BG)
|
||||||
|
drawStand()
|
||||||
|
drawWheelFull(0, nil)
|
||||||
|
drawResultText({ "Pull lever to spin!" })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stop()
|
||||||
|
if gpu then gpu.fill(COL_BG); gpu.sync() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function main()
|
||||||
|
while true do
|
||||||
|
waitForRedstonePulse()
|
||||||
|
|
||||||
|
drawResultText({ "SPINNING..." })
|
||||||
|
|
||||||
|
local winIdx = spin()
|
||||||
|
local prize = segments[winIdx]
|
||||||
|
|
||||||
|
drawResultText({ "WINNER!", prize.name })
|
||||||
|
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
-- Redraw clean with a fresh random idle angle
|
||||||
|
gpu.fill(COL_BG)
|
||||||
|
drawStand()
|
||||||
|
drawWheelFull(math.random() * TWO_PI, nil)
|
||||||
|
drawResultText({ "Pull lever to spin!" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { start = start, stop = stop, main = main }
|
||||||
@@ -65,22 +65,35 @@ local COL_WHITE = 0xFFFFFF
|
|||||||
local COL_BALL = 0xF0F0F0
|
local COL_BALL = 0xF0F0F0
|
||||||
local COL_BALL_SHD = 0x444444
|
local COL_BALL_SHD = 0x444444
|
||||||
|
|
||||||
-- Ball physics
|
-- Ball physics — 3-D simulation, top-down projected to 2-D screen
|
||||||
local BALL_RADIUS = 8 -- px
|
-- World units: 1 unit = 1 pixel at the wheel centre plane (z = 0).
|
||||||
local BALL_SPEED_MIN = 900 -- px/s initial tangential speed
|
-- z is the vertical axis (positive = up). Gravity points -z.
|
||||||
local BALL_SPEED_MAX = 1300
|
-- The bowl geometry is a truncated cone:
|
||||||
local TRACK_RESTITUTION = 0.82 -- speed fraction kept on track-wall bounce
|
-- Outer track : r = R_WORLD_OUT, z = Z_TRACK (rim, highest)
|
||||||
local POCKET_RESTITUTION = 0.52 -- speed fraction kept bouncing inside pocket ring
|
-- Inner wall : r = R_WORLD_IN, z = Z_DEFLECT (slightly lower)
|
||||||
local FRICTION_TRACK = 0.9985 -- multiplier per frame while in track
|
-- Pocket ring : r in [R_WORLD_PKT_IN, R_WORLD_OUT], z = Z_POCKET (lowest)
|
||||||
local FRICTION_POCKET = 0.972 -- higher damping once in pocket ring
|
-- All radii are set at runtime from R_OUTER / R_POCKET_* in world pixels.
|
||||||
-- Centripetal slide: inward acceleration applied as ball slows, simulating
|
|
||||||
-- the ball losing grip and sliding down the slope toward the centre.
|
local BALL_RADIUS = 8 -- px (screen drawing radius)
|
||||||
local SLIDE_ACCEL = 380 -- px/s² inward pull (scales with 1/speed)
|
local BALL_WORLD_R = 5 -- physics sphere radius in world units
|
||||||
local SLIDE_THRESHOLD = 500 -- px/s below this speed the slide kicks in
|
-- Initial tangential speed (world units / s)
|
||||||
-- Ball enters pocket ring when speed drops below this
|
local BALL_SPEED_MIN = 700
|
||||||
local DROP_SPEED = 80 -- px/s
|
local BALL_SPEED_MAX = 1000
|
||||||
-- Small random kick angle on each wall bounce
|
-- Gravity (world units / s²)
|
||||||
local BOUNCE_KICK_MAX = 0.10 -- rad
|
local GRAVITY = 1800
|
||||||
|
-- Bowl cone half-angle from horizontal (radians) — steeper = faster slide
|
||||||
|
local BOWL_SLOPE = math.pi / 9 -- 20 degrees
|
||||||
|
-- Pocket well is deeper — steeper slope
|
||||||
|
local POCKET_SLOPE = math.pi / 5 -- 36 degrees
|
||||||
|
-- Restitution on bowl surface normal bounce
|
||||||
|
local RESTITUTION_WALL = 0.55
|
||||||
|
local RESTITUTION_POCKET = 0.38
|
||||||
|
-- Rolling friction: velocity multiplier per second on the bowl surface
|
||||||
|
local FRICTION_ROLL = 0.988 -- per frame on track
|
||||||
|
local FRICTION_POCKET = 0.970 -- per frame in pocket
|
||||||
|
-- Ball drops from track into pocket ring when its radial position
|
||||||
|
-- crosses the inner deflector radius
|
||||||
|
local BOUNCE_KICK_MAX = 0.08 -- rad random angular kick on rim bounce
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- GPU / pixel primitives
|
-- GPU / pixel primitives
|
||||||
@@ -270,183 +283,235 @@ local function drawCenterText(lines, textSize)
|
|||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Physics spin — Cartesian 2-D ball, static wheel
|
-- Physics spin — full 3-D ball simulation, top-down projected to 2-D
|
||||||
--
|
--
|
||||||
-- Ball position: (bx, by) in pixel space
|
-- Ball position: (bx, by, bz) in world space
|
||||||
-- Ball velocity: (vx, vy) in px/s
|
-- Ball velocity: (vx, vy, vz) in world units/s
|
||||||
--
|
--
|
||||||
-- Track outer wall : circle of radius R_WALL_OUT centred on (CX, CY)
|
-- z = vertical axis (up positive), gravity = -z
|
||||||
-- Track inner wall : circle of radius R_WALL_IN
|
-- x, y map 1:1 to screen pixels relative to (CX, CY)
|
||||||
-- Pocket ring : between R_POCKET_IN and R_POCKET_OUT
|
|
||||||
--
|
--
|
||||||
-- Collision response: reflect velocity along the surface normal (radial
|
-- Bowl surface: cone frustum.
|
||||||
-- direction), apply restitution, add small random kick to angle.
|
-- TRACK phase : outer sloped ring, ball spirals inward as energy drops
|
||||||
|
-- POCKET phase : steeper inner bowl, ball bounces until settled
|
||||||
|
--
|
||||||
|
-- Each frame:
|
||||||
|
-- 1. Apply gravity to vz
|
||||||
|
-- 2. Project velocity onto bowl surface (normal-force constraint)
|
||||||
|
-- 3. Apply rolling friction
|
||||||
|
-- 4. Integrate position
|
||||||
|
-- 5. Snap bz to bowl surface z
|
||||||
|
-- 6. Handle radial wall collisions
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local PHASE_TRACK = 1
|
||||||
|
local PHASE_POCKET = 2
|
||||||
|
|
||||||
local function spin()
|
local function spin()
|
||||||
local dt = FRAME_DELAY
|
local dt = FRAME_DELAY
|
||||||
|
|
||||||
local R_WALL_OUT = R_OUTER - 6 - BALL_RADIUS
|
-- ── World-space geometry (radii in world px, heights in world px) ──
|
||||||
local R_WALL_IN = R_POCKET_OUT + 2 + BALL_RADIUS
|
local RW_OUT = R_OUTER - 6 - BALL_WORLD_R -- outer rim
|
||||||
local R_PKT_OUT = R_POCKET_OUT - BALL_RADIUS
|
local RW_IN = R_POCKET_OUT + 2 + BALL_WORLD_R -- inner deflector
|
||||||
local R_PKT_IN = R_POCKET_IN + BALL_RADIUS
|
local RW_PKT_OUT = R_POCKET_OUT - BALL_WORLD_R -- pocket outer wall
|
||||||
|
local RW_PKT_IN = R_POCKET_IN + BALL_WORLD_R -- pocket inner wall
|
||||||
local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2
|
local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2
|
||||||
|
|
||||||
-- Start ball at a random angle on the outer track, moving tangentially
|
-- Height of the bowl surface at a given radius:
|
||||||
|
-- Track: z = (r - RW_IN) * tan(BOWL_SLOPE) (zero at inner wall, rises outward)
|
||||||
|
-- Pocket: z = -(r - RW_IN) * tan(POCKET_SLOPE) (drops inward past deflector)
|
||||||
|
local tanTrack = math.tan(BOWL_SLOPE)
|
||||||
|
local tanPocket = math.tan(POCKET_SLOPE)
|
||||||
|
|
||||||
|
local function bowlZ(r, phase)
|
||||||
|
if phase == PHASE_POCKET then
|
||||||
|
return -(r - RW_PKT_OUT) * tanPocket
|
||||||
|
else
|
||||||
|
return (r - RW_IN) * tanTrack
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Surface outward normal in (radial, z) 2-D cross-section:
|
||||||
|
-- Track cone slopes up outward → normal = (sin θ, cos θ) rotated into 3D
|
||||||
|
-- radially. The 3-D normal = (nr * x/r, nr * y/r, nz)
|
||||||
|
local function bowlNormal(x, y, phase)
|
||||||
|
local r = math.sqrt(x*x + y*y)
|
||||||
|
if r < 0.001 then return 0, 0, 1 end
|
||||||
|
-- In the (r,z) plane the slope angle gives:
|
||||||
|
-- track: normal points inward-upward = (-sin θ, cos θ)
|
||||||
|
-- pocket: normal points outward-upward = ( sin θ, cos θ)
|
||||||
|
local nr, nz
|
||||||
|
if phase == PHASE_POCKET then
|
||||||
|
nr = math.sin(POCKET_SLOPE)
|
||||||
|
nz = math.cos(POCKET_SLOPE)
|
||||||
|
else
|
||||||
|
nr = -math.sin(BOWL_SLOPE)
|
||||||
|
nz = math.cos(BOWL_SLOPE)
|
||||||
|
end
|
||||||
|
-- Expand into 3D radially
|
||||||
|
local rx = x / r
|
||||||
|
local ry = y / r
|
||||||
|
return nr * rx, nr * ry, nz
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Initial conditions ─────────────────────────────────────────────
|
||||||
local startAngle = math.random() * TWO_PI
|
local startAngle = math.random() * TWO_PI
|
||||||
local startSpeed = BALL_SPEED_MIN + math.random() * (BALL_SPEED_MAX - BALL_SPEED_MIN)
|
local startSpeed = BALL_SPEED_MIN + math.random() * (BALL_SPEED_MAX - BALL_SPEED_MIN)
|
||||||
-- Tangential direction (perpendicular to radial, CCW = 90° CCW from outward normal)
|
local r0 = RW_OUT - 2
|
||||||
-- Outward normal at angle a: (cos a, sin a)
|
|
||||||
-- CCW tangent: (-sin a, cos a)
|
local bx = math.cos(startAngle) * r0
|
||||||
local bx = CX + math.cos(startAngle) * (R_WALL_OUT - 2)
|
local by = math.sin(startAngle) * r0
|
||||||
local by = CY + math.sin(startAngle) * (R_WALL_OUT - 2)
|
local bz = bowlZ(r0, PHASE_TRACK)
|
||||||
|
|
||||||
|
-- Start tangentially
|
||||||
local vx = -math.sin(startAngle) * startSpeed
|
local vx = -math.sin(startAngle) * startSpeed
|
||||||
local vy = math.cos(startAngle) * startSpeed
|
local vy = math.cos(startAngle) * startSpeed
|
||||||
|
local vz = 0.0
|
||||||
|
|
||||||
local inPocket = false
|
local phase = PHASE_TRACK
|
||||||
local elapsed = 0
|
local elapsed = 0
|
||||||
local MAX_TIME = 20.0
|
local MAX_TIME = 25.0
|
||||||
|
|
||||||
-- Draw initial ball position (wheel already on screen)
|
-- Project 3D (bx,by) → screen (sx,sy) (z is depth only, not projected)
|
||||||
drawBall(bx, by)
|
local function toScreen(x, y)
|
||||||
|
return CX + x, CY + y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Draw ball at current world position
|
||||||
|
local function drawBall3()
|
||||||
|
local sx, sy = toScreen(bx, by)
|
||||||
|
drawBall(sx, sy)
|
||||||
|
end
|
||||||
|
local function eraseBall3()
|
||||||
|
local sx, sy = toScreen(bx, by)
|
||||||
|
-- record for eraseBall's globals
|
||||||
|
ballX = math.floor(sx)
|
||||||
|
ballY = math.floor(sy)
|
||||||
|
eraseBall()
|
||||||
|
end
|
||||||
|
|
||||||
|
drawBall3()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
while elapsed < MAX_TIME do
|
while elapsed < MAX_TIME do
|
||||||
local speed = math.sqrt(vx*vx + vy*vy)
|
-- ── 1. Gravity ─────────────────────────────────────────────────
|
||||||
|
vz = vz - GRAVITY * dt
|
||||||
|
|
||||||
-- Apply friction
|
-- ── 2. Surface constraint ──────────────────────────────────────
|
||||||
local fric = inPocket and FRICTION_POCKET or FRICTION_TRACK
|
-- Project velocity onto the bowl surface (remove normal component).
|
||||||
|
-- This simulates the ball being pressed against the bowl by the
|
||||||
|
-- normal force, keeping it on the surface.
|
||||||
|
local r = math.sqrt(bx*bx + by*by)
|
||||||
|
local nx3, ny3, nz3 = bowlNormal(bx, by, phase)
|
||||||
|
local vdotn = vx*nx3 + vy*ny3 + vz*nz3
|
||||||
|
-- Only cancel the component pushing INTO the surface (vdotn < 0)
|
||||||
|
if vdotn < 0 then
|
||||||
|
vx = vx - vdotn * nx3
|
||||||
|
vy = vy - vdotn * ny3
|
||||||
|
vz = vz - vdotn * nz3
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── 3. Rolling friction ────────────────────────────────────────
|
||||||
|
local fric = (phase == PHASE_POCKET) and FRICTION_POCKET or FRICTION_ROLL
|
||||||
vx = vx * fric
|
vx = vx * fric
|
||||||
vy = vy * fric
|
vy = vy * fric
|
||||||
|
vz = vz * fric
|
||||||
|
|
||||||
-- Centripetal slide: as the ball slows it loses centripetal support
|
-- ── 4. Integrate ───────────────────────────────────────────────
|
||||||
-- and slides inward, like a real ball on a tilted cone/bowl.
|
|
||||||
if not inPocket and speed < SLIDE_THRESHOLD and speed > DROP_SPEED then
|
|
||||||
local dx0 = bx - CX
|
|
||||||
local dy0 = by - CY
|
|
||||||
local d0 = math.sqrt(dx0*dx0 + dy0*dy0)
|
|
||||||
if d0 > 0 then
|
|
||||||
-- Inward unit vector
|
|
||||||
local inx = -dx0 / d0
|
|
||||||
local iny = -dy0 / d0
|
|
||||||
-- Acceleration scales up as speed decreases
|
|
||||||
local accel = SLIDE_ACCEL * (1 - speed / SLIDE_THRESHOLD)
|
|
||||||
vx = vx + inx * accel * dt
|
|
||||||
vy = vy + iny * accel * dt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Integrate
|
|
||||||
bx = bx + vx * dt
|
bx = bx + vx * dt
|
||||||
by = by + vy * dt
|
by = by + vy * dt
|
||||||
|
bz = bz + vz * dt
|
||||||
|
|
||||||
-- Distance from centre
|
-- ── 5. Constrain z to bowl surface (snap) ─────────────────────
|
||||||
local dx = bx - CX
|
r = math.sqrt(bx*bx + by*by)
|
||||||
local dy = by - CY
|
local targetZ = bowlZ(r, phase)
|
||||||
local dist = math.sqrt(dx*dx + dy*dy)
|
bz = targetZ -- hard constraint keeps ball on surface
|
||||||
-- Outward unit normal
|
|
||||||
local nx = dx / dist
|
|
||||||
local ny = dy / dist
|
|
||||||
|
|
||||||
if not inPocket then
|
-- ── 6. Wall collisions ─────────────────────────────────────────
|
||||||
-- ── Outer wall bounce ───────────────────────────────────
|
if phase == PHASE_TRACK then
|
||||||
if dist > R_WALL_OUT then
|
-- Outer rim
|
||||||
-- Push back inside
|
if r > RW_OUT then
|
||||||
bx = CX + nx * R_WALL_OUT
|
local scale = RW_OUT / r
|
||||||
by = CY + ny * R_WALL_OUT
|
bx = bx * scale; by = by * scale
|
||||||
-- Reflect radial component
|
-- Radial inward normal for bounce
|
||||||
local vn = vx*nx + vy*ny
|
local rnx, rny = -bx/RW_OUT, -by/RW_OUT
|
||||||
vx = vx - 2*vn*nx; vy = vy - 2*vn*ny
|
local vn = vx*rnx + vy*rny
|
||||||
-- Apply restitution to the reflected (now inward) normal part
|
if vn < 0 then
|
||||||
local vn2 = vx*nx + vy*ny
|
vx = vx - 2*vn*rnx*(RESTITUTION_WALL)
|
||||||
vx = vx - vn2*nx*(1 - TRACK_RESTITUTION)
|
vy = vy - 2*vn*rny*(RESTITUTION_WALL)
|
||||||
vy = vy - vn2*ny*(1 - TRACK_RESTITUTION)
|
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
||||||
-- Small random angular kick
|
local c, s = math.cos(kick), math.sin(kick)
|
||||||
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
vx, vy = vx*c - vy*s, vx*s + vy*c
|
||||||
local c, s = math.cos(kick), math.sin(kick)
|
end
|
||||||
vx, vy = vx*c - vy*s, vx*s + vy*c
|
end
|
||||||
|
-- Inner deflector — cross into pocket phase
|
||||||
|
if r < RW_IN then
|
||||||
|
phase = PHASE_POCKET
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Enter pocket ring when slow enough ──────────────────
|
elseif phase == PHASE_POCKET then
|
||||||
if speed < DROP_SPEED and dist >= R_WALL_IN - 4 then
|
-- Outer pocket wall
|
||||||
inPocket = true
|
if r > RW_PKT_OUT then
|
||||||
|
local scale = RW_PKT_OUT / r
|
||||||
|
bx = bx * scale; by = by * scale
|
||||||
|
local rnx, rny = -bx/RW_PKT_OUT, -by/RW_PKT_OUT
|
||||||
|
local vn = vx*rnx + vy*rny
|
||||||
|
if vn < 0 then
|
||||||
|
vx = vx - 2*vn*rnx*RESTITUTION_POCKET
|
||||||
|
vy = vy - 2*vn*rny*RESTITUTION_POCKET
|
||||||
|
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
||||||
|
local c, s = math.cos(kick), math.sin(kick)
|
||||||
|
vx, vy = vx*c - vy*s, vx*s + vy*c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Inner pocket wall
|
||||||
|
if r < RW_PKT_IN then
|
||||||
|
local scale = RW_PKT_IN / r
|
||||||
|
bx = bx * scale; by = by * scale
|
||||||
|
local rnx, rny = bx/RW_PKT_IN, by/RW_PKT_IN
|
||||||
|
local vn = vx*rnx + vy*rny
|
||||||
|
if vn < 0 then
|
||||||
|
vx = vx - 2*vn*rnx*RESTITUTION_POCKET
|
||||||
|
vy = vy - 2*vn*rny*RESTITUTION_POCKET
|
||||||
|
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
||||||
|
local c, s = math.cos(kick), math.sin(kick)
|
||||||
|
vx, vy = vx*c - vy*s, vx*s + vy*c
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Inner wall bounce (deflector tip) ───────────────────
|
-- Settled when horizontal speed is very low
|
||||||
if dist < R_WALL_IN and not inPocket then
|
local hspd = math.sqrt(vx*vx + vy*vy)
|
||||||
bx = CX + nx * R_WALL_IN
|
if hspd < 5 then break end
|
||||||
by = CY + ny * R_WALL_IN
|
|
||||||
local vn = vx*nx + vy*ny
|
|
||||||
vx = vx - 2*vn*nx; vy = vy - 2*vn*ny
|
|
||||||
local vn2 = vx*nx + vy*ny
|
|
||||||
vx = vx - vn2*nx*(1 - TRACK_RESTITUTION)
|
|
||||||
vy = vy - vn2*ny*(1 - TRACK_RESTITUTION)
|
|
||||||
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
|
||||||
local c, s = math.cos(kick), math.sin(kick)
|
|
||||||
vx, vy = vx*c - vy*s, vx*s + vy*c
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- ── Inside pocket ring ───────────────────────────────────
|
|
||||||
-- Bounce off outer pocket wall
|
|
||||||
if dist > R_PKT_OUT then
|
|
||||||
bx = CX + nx * R_PKT_OUT
|
|
||||||
by = CY + ny * R_PKT_OUT
|
|
||||||
local vn = vx*nx + vy*ny
|
|
||||||
vx = vx - 2*vn*nx; vy = vy - 2*vn*ny
|
|
||||||
local vn2 = vx*nx + vy*ny
|
|
||||||
vx = vx - vn2*nx*(1 - POCKET_RESTITUTION)
|
|
||||||
vy = vy - vn2*ny*(1 - POCKET_RESTITUTION)
|
|
||||||
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
|
||||||
local c, s = math.cos(kick), math.sin(kick)
|
|
||||||
vx, vy = vx*c - vy*s, vx*s + vy*c
|
|
||||||
end
|
|
||||||
-- Bounce off inner pocket wall
|
|
||||||
if dist < R_PKT_IN then
|
|
||||||
bx = CX + nx * R_PKT_IN
|
|
||||||
by = CY + ny * R_PKT_IN
|
|
||||||
local vn = vx*nx + vy*ny
|
|
||||||
vx = vx - 2*vn*nx; vy = vy - 2*vn*ny
|
|
||||||
local vn2 = vx*nx + vy*ny
|
|
||||||
vx = vx - vn2*nx*(1 - POCKET_RESTITUTION)
|
|
||||||
vy = vy - vn2*ny*(1 - POCKET_RESTITUTION)
|
|
||||||
local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2
|
|
||||||
local c, s = math.cos(kick), math.sin(kick)
|
|
||||||
vx, vy = vx*c - vy*s, vx*s + vy*c
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Settled?
|
|
||||||
if speed < 6 then break end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
eraseBall()
|
eraseBall3()
|
||||||
drawBall(bx, by)
|
drawBall3()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(dt)
|
sleep(dt)
|
||||||
elapsed = elapsed + dt
|
elapsed = elapsed + dt
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Final position
|
-- Final draw
|
||||||
eraseBall()
|
eraseBall3()
|
||||||
drawBall(bx, by)
|
drawBall3()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
-- Nearest pocket by angle
|
-- Nearest pocket by angle of final (bx, by)
|
||||||
local finalAngle = math.atan2(by - CY, bx - CX)
|
local finalAngle = math.atan2(by, bx)
|
||||||
local bestSlot, bestDist = 1, math.huge
|
local bestSlot, bestDist = 1, math.huge
|
||||||
for i = 1, NUM_POCKETS do
|
for i = 1, NUM_POCKETS do
|
||||||
local sa = ((i - 1) * TWO_PI / NUM_POCKETS) % TWO_PI
|
local sa = ((i - 1) * TWO_PI / NUM_POCKETS) % TWO_PI
|
||||||
-- normalise finalAngle to [0, 2pi)
|
|
||||||
local fa = finalAngle % TWO_PI
|
local fa = finalAngle % TWO_PI
|
||||||
local diff = math.abs(sa - fa)
|
local diff = math.abs(sa - fa)
|
||||||
if diff > math.pi then diff = TWO_PI - diff end
|
if diff > math.pi then diff = TWO_PI - diff end
|
||||||
if diff < bestDist then bestDist = diff; bestSlot = i end
|
if diff < bestDist then bestDist = diff; bestSlot = i end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Snap to pocket centre
|
-- Snap ball to pocket centre on screen
|
||||||
local snapAngle = FIXED_ROTOR + (bestSlot - 1) * TWO_PI / NUM_POCKETS
|
local snapAngle = FIXED_ROTOR + (bestSlot - 1) * TWO_PI / NUM_POCKETS
|
||||||
local sx = CX + math.cos(snapAngle) * R_SETTLE
|
local sx = CX + math.cos(snapAngle) * R_SETTLE
|
||||||
local sy = CY + math.sin(snapAngle) * R_SETTLE
|
local sy = CY + math.sin(snapAngle) * R_SETTLE
|
||||||
eraseBall()
|
eraseBall3()
|
||||||
drawBall(sx, sy)
|
drawBall(sx, sy)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user