updated
This commit is contained in:
@@ -68,10 +68,10 @@ local function buildSegments()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Spin physics
|
-- Spin physics
|
||||||
local OMEGA_MIN = 4.0 -- rad/s
|
local OMEGA_MIN = 8.0 -- rad/s fast launch
|
||||||
local OMEGA_MAX = 10.0
|
local OMEGA_MAX = 16.0
|
||||||
local FRICTION = 0.987 -- multiplier per frame
|
local FRICTION = 0.980 -- per frame — stops cleanly in ~4s
|
||||||
local STOP_OMEGA = 0.04 -- rad/s
|
local STOP_OMEGA = 0.10 -- rad/s clean stop threshold, no wiggle
|
||||||
|
|
||||||
-- Colours
|
-- Colours
|
||||||
local COL_BG = 0x050505
|
local COL_BG = 0x050505
|
||||||
@@ -136,85 +136,110 @@ local function px_text_centre(str, y, fg, bg, size)
|
|||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Wheel drawing
|
-- Wheel drawing — single-pass rasteriser
|
||||||
|
--
|
||||||
|
-- Instead of drawing each wedge separately (N passes over the full
|
||||||
|
-- bounding box), we do ONE pass: for every pixel inside the wheel
|
||||||
|
-- annulus, compute its angle and look up the wedge colour.
|
||||||
|
-- This is N× faster and eliminates the per-wedge sleep(0) during spin.
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local segments = {}
|
local segments = {}
|
||||||
|
|
||||||
local function drawWedge(seg, wheelAngle, glowing)
|
-- Build a flat lookup: given a wheel-local angle in [0, TWO_PI),
|
||||||
local col = seg.col
|
-- return the segment index. Linear scan over 12 segments is fine.
|
||||||
if glowing then
|
local function segmentForAngle(a)
|
||||||
local r = math.min(255, math.floor(col / 0x10000) + 80)
|
for i, seg in ipairs(segments) do
|
||||||
local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 80)
|
if a >= seg.startA and a < seg.endA then return i end
|
||||||
local b = math.min(255, col % 0x100 + 80)
|
|
||||||
col = r * 0x10000 + g * 0x100 + b
|
|
||||||
end
|
end
|
||||||
|
return #segments -- wrap-around safety
|
||||||
local ri = R_POCKET_IN
|
|
||||||
local ro = R_OUTER
|
|
||||||
local a0 = seg.startA + wheelAngle
|
|
||||||
local arc = seg.endA - seg.startA
|
|
||||||
|
|
||||||
local bx0 = math.floor(CX - ro) - 1
|
|
||||||
local bx1 = math.ceil (CX + ro) + 1
|
|
||||||
local by0 = math.floor(CY - ro) - 1
|
|
||||||
local by1 = math.ceil (CY + ro) + 1
|
|
||||||
|
|
||||||
for sy = by0, by1 do
|
|
||||||
local dy = sy - CY
|
|
||||||
local runStart = nil
|
|
||||||
for sx = bx0, bx1 do
|
|
||||||
local dx = sx - CX
|
|
||||||
local dist = math.sqrt(dx*dx + dy*dy)
|
|
||||||
if dist >= ri and dist <= ro then
|
|
||||||
local rel = (math.atan2(dy, dx) - 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
|
|
||||||
else
|
|
||||||
if runStart then
|
|
||||||
px_rect(runStart, sy, sx - runStart, 1, col)
|
|
||||||
runStart = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if runStart then
|
|
||||||
px_rect(runStart, sy, bx1 - runStart + 1, 1, col)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Separator spoke at startA
|
|
||||||
local a0w = seg.startA + wheelAngle
|
|
||||||
for i = 0, math.floor(ro - ri) do
|
|
||||||
local r = ri + i
|
|
||||||
local sx = CX + math.cos(a0w) * r
|
|
||||||
local sy = CY + math.sin(a0w) * r
|
|
||||||
px_rect(math.floor(sx), math.floor(sy), 2, 1, COL_SEP)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Label at 72% radius, wedge midpoint
|
|
||||||
local midA = seg.midA + wheelAngle
|
|
||||||
local lr = (ri + ro) * 0.72
|
|
||||||
local lx = CX + math.cos(midA) * lr
|
|
||||||
local ly = CY + math.sin(midA) * lr
|
|
||||||
px_text(seg.name, lx - math.floor(#seg.name * 3), ly - 4, COL_WHITE, col, 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawAllWedges(wheelAngle, glowIdx)
|
-- Draw the full wheel in one raster pass.
|
||||||
|
-- wheelAngle: current rotation offset (added to every wedge startA/endA).
|
||||||
|
-- glowIdx: if non-nil, that segment is brightened.
|
||||||
|
local function drawWheel(wheelAngle, glowIdx)
|
||||||
|
-- Precompute brightened colours so we don't recompute inside the loop
|
||||||
|
local cols = {}
|
||||||
for i, seg in ipairs(segments) do
|
for i, seg in ipairs(segments) do
|
||||||
drawWedge(seg, wheelAngle, i == glowIdx)
|
if i == glowIdx then
|
||||||
sleep(0)
|
local c = seg.col
|
||||||
|
local r = math.min(255, math.floor(c / 0x10000) + 80)
|
||||||
|
local g = math.min(255, math.floor((c % 0x10000) / 0x100) + 80)
|
||||||
|
local b = math.min(255, c % 0x100 + 80)
|
||||||
|
cols[i] = r * 0x10000 + g * 0x100 + b
|
||||||
|
else
|
||||||
|
cols[i] = seg.col
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ri = R_POCKET_IN
|
||||||
|
local ro = R_OUTER
|
||||||
|
local ri2 = ri * ri
|
||||||
|
local ro2 = ro * ro
|
||||||
|
|
||||||
|
-- Normalise wheelAngle so angles stay in a sane range
|
||||||
|
local wa = wheelAngle % TWO_PI
|
||||||
|
|
||||||
|
for sy = math.floor(CY - ro) - 1, math.ceil(CY + ro) + 1 do
|
||||||
|
local dy = sy - CY
|
||||||
|
local dy2 = dy * dy
|
||||||
|
if dy2 <= ro2 then
|
||||||
|
local xhalf = math.floor(math.sqrt(ro2 - dy2) + 0.5)
|
||||||
|
local runStart = nil
|
||||||
|
local runCol = nil
|
||||||
|
for sx = CX - xhalf, CX + xhalf do
|
||||||
|
local dx = sx - CX
|
||||||
|
local dx2 = dx * dx
|
||||||
|
local d2 = dx2 + dy2
|
||||||
|
local col = nil
|
||||||
|
if d2 >= ri2 and d2 <= ro2 then
|
||||||
|
-- Inside annulus — find wedge
|
||||||
|
-- atan2 returns angle in world space; subtract wheelAngle
|
||||||
|
-- to get wheel-local angle, then mod into [0, TWO_PI)
|
||||||
|
local worldA = math.atan2(dy, dx)
|
||||||
|
local localA = (worldA - wa) % TWO_PI
|
||||||
|
local idx = segmentForAngle(localA)
|
||||||
|
col = cols[idx]
|
||||||
|
end
|
||||||
|
if col == runCol then
|
||||||
|
-- extend run (including col==nil for outside-annulus gaps)
|
||||||
|
else
|
||||||
|
if runCol and runStart then
|
||||||
|
px_rect(runStart, sy, sx - runStart, 1, runCol)
|
||||||
|
end
|
||||||
|
runStart = sx
|
||||||
|
runCol = col
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- flush last run
|
||||||
|
if runCol and runStart then
|
||||||
|
local xend = CX + xhalf + 1
|
||||||
|
px_rect(runStart, sy, xend - runStart, 1, runCol)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Separator spokes — draw after fill so they appear on top
|
||||||
|
for i, seg in ipairs(segments) do
|
||||||
|
local a = seg.startA + wa
|
||||||
|
for step = 0, math.floor(R_OUTER - R_POCKET_IN) do
|
||||||
|
local rr = R_POCKET_IN + step
|
||||||
|
px_rect(math.floor(CX + math.cos(a) * rr),
|
||||||
|
math.floor(CY + math.sin(a) * rr), 2, 1, COL_SEP)
|
||||||
|
end
|
||||||
|
-- Label
|
||||||
|
local midA = seg.midA + wa
|
||||||
|
local lr = (R_POCKET_IN + R_OUTER) * 0.68
|
||||||
|
local lx = CX + math.cos(midA) * lr
|
||||||
|
local ly = CY + math.sin(midA) * lr
|
||||||
|
px_text(seg.name, lx - math.floor(#seg.name * 3), ly - 4,
|
||||||
|
COL_WHITE, cols[i], 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawChrome()
|
local function drawChrome()
|
||||||
px_annulus(CX, CY, R_OUTER, R_OUTER + 7, COL_RIM)
|
px_annulus(CX, CY, R_OUTER, R_OUTER + 7, COL_RIM)
|
||||||
-- Hub
|
|
||||||
px_circle(CX, CY, R_POCKET_IN, COL_HUB)
|
px_circle(CX, CY, R_POCKET_IN, COL_HUB)
|
||||||
px_annulus(CX, CY, R_POCKET_IN - 4, R_POCKET_IN, COL_HUB_RING)
|
px_annulus(CX, CY, R_POCKET_IN - 4, R_POCKET_IN, COL_HUB_RING)
|
||||||
px_circle(CX, CY, 6, COL_HUB_RING)
|
px_circle(CX, CY, 6, COL_HUB_RING)
|
||||||
@@ -255,7 +280,7 @@ end
|
|||||||
|
|
||||||
local function drawWheelFull(wheelAngle, glowIdx)
|
local function drawWheelFull(wheelAngle, glowIdx)
|
||||||
px_circle(CX, CY, R_OUTER + 9, COL_BG)
|
px_circle(CX, CY, R_OUTER + 9, COL_BG)
|
||||||
drawAllWedges(wheelAngle, glowIdx)
|
drawWheel(wheelAngle, glowIdx)
|
||||||
drawChrome()
|
drawChrome()
|
||||||
drawPointer()
|
drawPointer()
|
||||||
end
|
end
|
||||||
@@ -281,38 +306,39 @@ end
|
|||||||
local function spin()
|
local function spin()
|
||||||
local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN)
|
local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN)
|
||||||
local angle = math.random() * TWO_PI
|
local angle = math.random() * TWO_PI
|
||||||
local elapsed = 0
|
|
||||||
|
|
||||||
|
-- Initial draw
|
||||||
drawWheelFull(angle, nil)
|
drawWheelFull(angle, nil)
|
||||||
drawHubText({ "SPINNING..." })
|
drawHubText({ "SPINNING..." })
|
||||||
|
|
||||||
while elapsed < 30.0 do
|
local frameCount = 0
|
||||||
|
while true do
|
||||||
omega = omega * FRICTION
|
omega = omega * FRICTION
|
||||||
angle = angle + omega * FRAME_DELAY
|
angle = angle + omega * FRAME_DELAY
|
||||||
if angle > TWO_PI * 100 then angle = angle % TWO_PI end
|
if angle > TWO_PI * 100 then angle = angle % TWO_PI end
|
||||||
|
|
||||||
px_circle(CX, CY, R_OUTER + 9, COL_BG)
|
-- drawWheel overwrites every annulus pixel — no need to clear first
|
||||||
drawAllWedges(angle, nil)
|
drawWheel(angle, nil)
|
||||||
drawChrome()
|
drawChrome()
|
||||||
drawPointer()
|
drawPointer()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
sleep(FRAME_DELAY)
|
-- Yield to OS every 4 frames to avoid CC timeout without a sleep(0) overhead
|
||||||
elapsed = elapsed + FRAME_DELAY
|
frameCount = frameCount + 1
|
||||||
|
if frameCount % 4 == 0 then sleep(0) end
|
||||||
|
|
||||||
if omega < STOP_OMEGA then break end
|
if omega < STOP_OMEGA then break end
|
||||||
end
|
end
|
||||||
|
|
||||||
local winIdx = segmentAtPointer(angle)
|
local winIdx = segmentAtPointer(angle)
|
||||||
|
|
||||||
-- Flash winning wedge
|
-- Flash winning wedge — 6 flashes at 120ms each
|
||||||
for flash = 1, 7 do
|
for flash = 1, 6 do
|
||||||
px_circle(CX, CY, R_OUTER + 9, COL_BG)
|
drawWheel(angle, flash % 2 == 1 and winIdx or nil)
|
||||||
drawAllWedges(angle, flash % 2 == 1 and winIdx or nil)
|
|
||||||
drawChrome()
|
drawChrome()
|
||||||
drawPointer()
|
drawPointer()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(0.14)
|
sleep(0.12)
|
||||||
end
|
end
|
||||||
|
|
||||||
return winIdx, angle
|
return winIdx, angle
|
||||||
|
|||||||
@@ -76,24 +76,23 @@ local COL_BALL_SHD = 0x444444
|
|||||||
|
|
||||||
local BALL_RADIUS = 8 -- px (screen drawing radius)
|
local BALL_RADIUS = 8 -- px (screen drawing radius)
|
||||||
local BALL_WORLD_R = 5 -- physics sphere radius in world units
|
local BALL_WORLD_R = 5 -- physics sphere radius in world units
|
||||||
-- Initial tangential speed (world units / s)
|
-- Initial tangential speed (world units / s) — very fast launch
|
||||||
local BALL_SPEED_MIN = 700
|
local BALL_SPEED_MIN = 1800
|
||||||
local BALL_SPEED_MAX = 1000
|
local BALL_SPEED_MAX = 2800
|
||||||
-- Gravity (world units / s²)
|
-- Gravity (world units / s²)
|
||||||
local GRAVITY = 1800
|
local GRAVITY = 1800
|
||||||
-- Bowl cone half-angle from horizontal (radians) — steeper = faster slide
|
-- Bowl cone half-angle from horizontal (radians)
|
||||||
local BOWL_SLOPE = math.pi / 9 -- 20 degrees
|
local BOWL_SLOPE = math.pi / 9 -- 20 degrees
|
||||||
-- Pocket well is deeper — steeper slope
|
|
||||||
local POCKET_SLOPE = math.pi / 5 -- 36 degrees
|
local POCKET_SLOPE = math.pi / 5 -- 36 degrees
|
||||||
-- Restitution on bowl surface normal bounce
|
-- Restitution on wall bounce
|
||||||
local RESTITUTION_WALL = 0.82
|
local RESTITUTION_WALL = 0.82
|
||||||
local RESTITUTION_POCKET = 0.68
|
local RESTITUTION_POCKET = 0.68
|
||||||
-- Rolling friction: velocity multiplier per second on the bowl surface
|
-- Rolling friction: multiplier per frame — strong enough to stop cleanly
|
||||||
local FRICTION_ROLL = 0.988 -- per frame on track
|
local FRICTION_ROLL = 0.975 -- per frame (~0.43/s at 33fps — decays in ~5s)
|
||||||
local FRICTION_POCKET = 0.970 -- per frame in pocket
|
local FRICTION_POCKET = 0.955 -- pocket damps faster
|
||||||
-- Ball drops from track into pocket ring when its radial position
|
-- Stop when horizontal speed drops below this — no wiggle
|
||||||
-- crosses the inner deflector radius
|
local STOP_SPEED = 18 -- px/s
|
||||||
local BOUNCE_KICK_MAX = 0.08 -- rad random angular kick on rim bounce
|
local BOUNCE_KICK_MAX = 0.08 -- rad
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- GPU / pixel primitives
|
-- GPU / pixel primitives
|
||||||
@@ -242,74 +241,62 @@ end
|
|||||||
local ballX, ballY = 0, 0
|
local ballX, ballY = 0, 0
|
||||||
|
|
||||||
-- Repair the wheel pixels inside a circle of radius r centred on (cx,cy).
|
-- Repair the wheel pixels inside a circle of radius r centred on (cx,cy).
|
||||||
-- Re-rasterises all wedges clipped to that bounding box, then chrome on top.
|
-- Single pass: for every pixel in the bounding box, compute the correct
|
||||||
-- This replaces the old flat-colour eraseBall which left coloured smears.
|
-- wheel colour and paint it. No flat-colour approximation.
|
||||||
local function repairWheelPatch(cx, cy, r)
|
local function repairWheelPatch(cx, cy, r)
|
||||||
cx = math.floor(cx); cy = math.floor(cy)
|
cx = math.floor(cx); cy = math.floor(cy)
|
||||||
-- Bounding box for the patch
|
|
||||||
local px0 = cx - r; local px1 = cx + r
|
local px0 = cx - r; local px1 = cx + r
|
||||||
local py0 = cy - r; local py1 = cy + r
|
local py0 = cy - r; local py1 = cy + r
|
||||||
|
|
||||||
-- For each wedge, re-rasterise only within this bounding box
|
-- Pre-compute per-wedge geometry (angle extents) once
|
||||||
|
local halfArc = math.pi / NUM_POCKETS
|
||||||
|
local wedges = {}
|
||||||
for slotIdx = 1, NUM_POCKETS do
|
for slotIdx = 1, NUM_POCKETS do
|
||||||
local halfArc = math.pi / NUM_POCKETS
|
local mid = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS
|
||||||
local midAngle = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS
|
wedges[slotIdx] = {
|
||||||
local a0 = midAngle - halfArc
|
a0 = mid - halfArc,
|
||||||
local a1 = midAngle + halfArc
|
arc = TWO_PI / NUM_POCKETS,
|
||||||
local arc = (a1 - a0) % TWO_PI
|
col = pocketColor(WHEEL_ORDER[slotIdx]),
|
||||||
|
}
|
||||||
local num = WHEEL_ORDER[slotIdx]
|
|
||||||
local col = pocketColor(num)
|
|
||||||
|
|
||||||
local ri, ro = R_POCKET_IN, R_POCKET_OUT
|
|
||||||
|
|
||||||
for sy = py0, py1 do
|
|
||||||
local runStart = nil
|
|
||||||
for sx = px0, px1 do
|
|
||||||
local dx = sx - CX; local dy = sy - CY
|
|
||||||
local dist = math.sqrt(dx*dx + dy*dy)
|
|
||||||
local inRing = dist >= ri and dist <= ro
|
|
||||||
local rel = (math.atan2(dy, dx) - a0) % TWO_PI
|
|
||||||
local inWedge = rel <= arc
|
|
||||||
if inRing and inWedge 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, px1 - runStart + 1, 1, col)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Repaint chrome rings and background that overlap the patch,
|
|
||||||
-- in inside-out priority order using elseif so each pixel is set once.
|
|
||||||
for sy = py0, py1 do
|
for sy = py0, py1 do
|
||||||
for sx = px0, px1 do
|
for sx = px0, px1 do
|
||||||
local dx = sx - CX; local dy = sy - CY
|
local dx = sx - CX
|
||||||
local d = math.sqrt(dx*dx + dy*dy)
|
local dy = sy - CY
|
||||||
|
local d = math.sqrt(dx*dx + dy*dy)
|
||||||
|
local col
|
||||||
|
|
||||||
|
-- Determine correct colour for this pixel, inside-out priority
|
||||||
if d <= R_HUB - 4 then
|
if d <= R_HUB - 4 then
|
||||||
px_rect(sx, sy, 1, 1, COL_HUB)
|
col = COL_HUB
|
||||||
elseif d <= R_HUB then
|
elseif d <= R_HUB then
|
||||||
px_rect(sx, sy, 1, 1, COL_HUB_RING)
|
col = COL_HUB_RING
|
||||||
|
elseif d <= R_POCKET_IN - 2 then
|
||||||
|
col = COL_HUB -- gap between hub ring and inner rim
|
||||||
elseif d <= R_POCKET_IN then
|
elseif d <= R_POCKET_IN then
|
||||||
-- gap between hub ring and inner rim — plain hub bg
|
col = COL_RIM
|
||||||
px_rect(sx, sy, 1, 1, COL_HUB)
|
|
||||||
elseif d <= R_POCKET_IN + 2 then
|
|
||||||
px_rect(sx, sy, 1, 1, COL_RIM)
|
|
||||||
elseif d <= R_POCKET_OUT then
|
elseif d <= R_POCKET_OUT then
|
||||||
-- inside pocket ring — wedge already painted above
|
-- Pocket ring: find which wedge this pixel belongs to
|
||||||
|
local angle = math.atan2(dy, dx)
|
||||||
|
for _, w in ipairs(wedges) do
|
||||||
|
local rel = (angle - w.a0) % TWO_PI
|
||||||
|
if rel <= w.arc then
|
||||||
|
col = w.col
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not col then col = COL_HUB end -- fallback
|
||||||
elseif d <= R_POCKET_OUT + 2 then
|
elseif d <= R_POCKET_OUT + 2 then
|
||||||
px_rect(sx, sy, 1, 1, COL_RIM)
|
col = COL_RIM
|
||||||
elseif d <= R_OUTER - 6 then
|
elseif d <= R_OUTER - 6 then
|
||||||
px_rect(sx, sy, 1, 1, COL_TRACK)
|
col = COL_TRACK
|
||||||
elseif d <= R_OUTER then
|
elseif d <= R_OUTER then
|
||||||
px_rect(sx, sy, 1, 1, COL_RIM)
|
col = COL_RIM
|
||||||
end
|
end
|
||||||
|
-- d > R_OUTER: outside wheel, leave as-is (ball never goes there)
|
||||||
|
|
||||||
|
if col then px_rect(sx, sy, 1, 1, col) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -545,7 +532,7 @@ local function spin()
|
|||||||
|
|
||||||
-- Settled when horizontal speed is very low
|
-- Settled when horizontal speed is very low
|
||||||
local hspd = math.sqrt(vx*vx + vy*vy)
|
local hspd = math.sqrt(vx*vx + vy*vy)
|
||||||
if hspd < 5 then break end
|
if hspd < STOP_SPEED then break end
|
||||||
end
|
end
|
||||||
|
|
||||||
eraseBall3()
|
eraseBall3()
|
||||||
|
|||||||
Reference in New Issue
Block a user