-- merged_cluster_gforce.lua — cluster UI + G-force polygon dot
-- Auto-generated merge

sim = ac.getSim()

--══════════════════════════════════════════════════════════════════════════════
--  cluster_plus_bars.lua — Combines cluster UI + masked alpha bar + dual boxes
--    • Based on: cluster.lua (live values + UV-cropped alpha-fill mask)
--    • And:     two_bars.lua (Oil Temp bar + Turbo spool bar)
--  Notes:
--    - Uses a single `update(dt)` entrypoint that draws everything.
--    - Helpers are de-duplicated; shared `clamp`/`rect` etc.
--    - two_bars code is namespaced lightly (Oil/Turbo) to avoid conflicts.
--══════════════════════════════════════════════════════════════════════════════

---------------------------------- USER SETTINGS ------------------------------
-- Speed / range
local useMph = false           -- false = km/h, true = mph
local fuelKmsPerLitre = 9      -- km per litre for the range readout

-- Masked alpha bar toggle + settings
local ENABLE_MASKED_ALPHA_BAR = true
local ALPHA_MASK_PATH = "cluster/urus_cluster_mask.png"  -- ← your mask
local BAR_POS  = vec2(0, 0)    -- top-left in the display texture
local BAR_SIZE = vec2(0, 0)    -- pixel size of the mask area
local BAR_FADE = 8             -- soft tip fade (pixels inside the fill)

-- Fraction source for the demo bar (0..1). Here: fuel fraction.
local function alphaBarFraction()
  if car and car.maxFuel and car.maxFuel > 0 then
    return math.max(0, math.min(1, (car.fuel or 0) / car.maxFuel))
  end
  return 0
end

---------------------------------- DISPLAY COLOURS ----------------------------
local white = rgbm(1, 1, 1,1)

---------------------------------- GEAR MAPS ----------------------------------
local gearT = { [-1]="R",[0]="N",[1]="1",[2]="2",[3]="3",[4]="4",[5]="5",[6]="6",[7]="7",[8]="8" }
local gearM = { [0]="N", [1]="D",[2]="D",[3]="D",[4]="4",[5]="5",[6]="6",[7]="7",[8]="8" }

---------------------------------- HELPERS ------------------------------------
local function v(str) local x,y=str:match("([^,]+),%s*([^,]+)"); return vec2(tonumber(x), tonumber(y)) end
local function lerp(a,b,t) return a + (b - a) * t end
local function clamp(val,a,b) return (val<a) and a or (val>b) and b or val end

---------------------------------- RECT DRAW HELPER ---------------------------
local function rect(x,y,w,h,c,opacity)
  display.rect{ pos=vec2(x,y), size=vec2(w,h), color=c, opacity=opacity or 1 }
end

---------------------------------- TURN SIGNALS -------------------------------
local indicatorActive = false

---------------------------------- FALLBACK: RECT-BASED ALPHA FILL -----------
local function drawAlphaFillBar(pos, size, fraction, fadePx)
  local w = math.floor(size.x * clamp(fraction or 0, 0, 1) + 0.5)
  if w <= 0 then return end
  local fade = math.max(tonumber(fadePx) or 0, 0)
  local solidW = math.max(w - fade, 0)
  if solidW > 0 then
    display.rect{ pos = pos, size = vec2(solidW, size.y), color = rgb(0,0,0), opacity = 1 }
  end
  local rest = w - solidW
  if rest > 0 then
    local slices = math.max(math.ceil(rest / 4), 4)
    local segW   = rest / slices
    for i = 0, slices - 1 do
      local a  = (i + 1) / slices
      local sx = pos.x + solidW + math.floor(i * segW + 0.5)
      local sw = math.ceil(segW)
      if sx + sw > pos.x + w then sw = pos.x + w - sx end
      if sw > 0 then
        display.rect{ pos = vec2(sx, pos.y), size = vec2(sw, size.y), color = rgb(0,0,0), opacity = a }
      end
    end
  end
end

---------------------------------- MASK-AWARE (UV-CROPPED) ALPHA FILL ---------
local _maskTex = nil
local function maskTex()
  if _maskTex ~= nil then return _maskTex end
  local ok, tex = pcall(function() return display.loadTexture(ALPHA_MASK_PATH) end)
  _maskTex = ok and tex or false
  return _maskTex
end

local function drawMaskedAlphaFillBar(pos, size, fraction, fadePx)
  local tex = maskTex()
  if not tex then
    return drawAlphaFillBar(pos, size, fraction, fadePx)
  end

  local f = clamp(fraction or 0, 0, 1)
  if f <= 0 then return end

  local filledPx = size.x * f
  display.image{
    texture = tex,
    pos     = pos,
    size    = vec2(filledPx, size.y),
    uvStart = vec2(0, 0),
    uvEnd   = vec2(f, 1),
    color   = rgbm(0, 0, 0, 1)  -- writes alpha only (RGB ignored by material)
  }

  local fade = math.max(tonumber(fadePx) or 0, 0)
  if fade > 0 then
    local fadeW  = math.min(fade, filledPx)
    local slices = math.max(math.ceil(fadeW / 4), 4)
    local segW   = fadeW / slices
    for i = 0, slices - 1 do
      local segPx0 = filledPx - fadeW + i * segW
      local segPx1 = segPx0 + segW
      local uv0 = segPx0 / size.x
      local uv1 = segPx1 / size.x
      local sx = pos.x + segPx0
      local sw = segW
      local a  = 1 - (i + 1) / slices     -- 1 → 0 toward the tip
      display.image{
        texture = tex,
        pos     = vec2(sx, pos.y),
        size    = vec2(sw, size.y),
        uvStart = vec2(uv0, 0),
        uvEnd   = vec2(uv1, 1),
        color   = rgbm(0, 0, 0, a)
      }
    end
  end
end



--══════════════════════════════════════════════════════════════════════════════
--                           STEP LED PROGRESS STRIPS
--   • WATER_TEMP: 8 LEDs, linear from 50°C → 130°C
--   • FUEL:       8 LEDs, linear from 0%  → 100%
--   How it works:
--     Your base cluster texture shows the LED icons. We draw **black covers**
--     over each LED by default. As progress increases, we stop drawing the
--     cover for the first N LEDs, thereby revealing them.
--   Setup:
--     Fill in the rectangles (x,y,w,h) for each LED below. Order them from
--     “first to light” → “last to light” along the strip.
--══════════════════════════════════════════════════════════════════════════════

-- Example rectangles (placeholders). Replace with real positions/sizes.
-- Tip: measure in AC showroom with your atlas resolution.
local WATER_LED_RECTS = {
  -- {x, y, w, h}
  {207.1, 1164,28,23.8},
  {192, 1140.9,27.4, 23.4},
  {179.9, 1116, 28.6, 24.2},
  {175.2, 1090.3,16.5, 25},
  {169.8, 1063.7,19, 24.9},
  {168.2, 1037.1,15.8, 25},
  {167.1, 1010.4, 11.7, 25.3},
  {165.3, 982.5,11.5, 27.2},
}

local FUEL_LED_RECTS = {
  -- Replace these with your fuel LED rects, ordered bottom→top or left→right
  {1823.3, 1166.2, 27.7, 24.2},
  {1839.6, 1142.8, 26.1, 23},
  {1854.9, 1117.8,21.7, 24.6},
  {1862.5, 1091.4,18.3, 25.9},
  {1870.2, 1065.3, 16.5, 25.4},
  {1873.7, 1039, 15.8, 25.4},
  {1878, 1012.1,13.3, 25.8},
  {1880.8, 984.6, 12.1, 26.9},
}

-- Draw covers for all LEDs except the first `lit` ones
local function drawStepLeds(rects, lit, coverColor, coverOpacity, corner)
  coverColor   = coverColor   or rgb(0,0,0)
  coverOpacity = coverOpacity or 1
  corner       = corner       or 0
  for i, r in ipairs(rects) do
    if i > lit then
      display.rect({
        pos     = vec2(r[1], r[2]),
        size    = vec2(r[3], r[4]),
        color   = coverColor,
        corner  = corner,
        border  = 0,
        opacity = coverOpacity
      })
    end
  end
end

-- Helper to compute how many LEDs to reveal from a 0..1 fraction
local function ledsFromFraction(frac, total)
  frac = clamp(frac or 0, 0, 1)
  total = total or 8
  -- Use ceil so first LED appears as soon as value crosses step
  local lit = math.ceil(frac * total)
  if lit < 0 then lit = 0 end
  if lit > total then lit = total end
  return lit
end
-- Optional: visual overlay to check mask alignment (toggle true temporarily)
local MASK_DEBUG_SHOW = false
local function debugDrawMask()
  if not MASK_DEBUG_SHOW then return end
  local tex = maskTex()
  if tex then
    display.image{ texture = tex, pos = BAR_POS, size = BAR_SIZE, color = rgbm(1,1,1,0.25) }
  end
end

--══════════════════════════════════════════════════════════════════════════════
--                          DISCRETE BOX BARS (Oil + Turbo)
--   (Adapted from two_bars.lua, with light namespacing and shared helpers)
--══════════════════════════════════════════════════════════════════════════════

-- OIL bar config (your original bar settings repurposed for oil temperature)
local BAR_OIL = { x = 448, y = 1241, w = 150, h = 11 }
local FILL_MODE_OIL    = "left"   -- "left" | "right" | "both"
local BOXES_PER_HALF   = 5
local BOXES_TOTAL      = 12
local BOX_GAP_PX       = 1
local COL_EDGE1        = rgb(1.00, 0.25, 0.00)
local COL_EDGE2        = rgb(1.00, 0.25, 0.00)
local COL_MID          = rgb(1.00, 0.25, 0.00)
local SPLIT1           = 0.50
local SPLIT2           = 0.50

-- "Red" behavior kept for compatibility (disabled by default)
local RED_RPM   = 6450
local RED_MODE  = "off"         -- "off" | "solid" | "blink" | "blink_hide"
local RED_COLOR = rgb(1.00, 0.25, 0.00)
local BLINK_HZ  = 4
local DRAW_OUTLINE = true
local OUTLINE_COL  = rgb(1,1,1)

-- Oil temperature mapping
local OIL_MIN_C = 50        -- 0% fill at 50°C
local OIL_MAX_C = 130       -- 100% fill at 130°C

-- TURBO bar config (independent)
local BAR_TURBO= { x = 1465, y = 1241, w = 150, h = 11 }
local TURBO_FILL_MODE   = "left"     -- "left" | "right" | "both"
local TURBO_BOXES_TOTAL = 12
local TURBO_BOXES_HALF  = 5
local TURBO_GAP_PX      = 1
local TURBO_COL_EDGE1   = rgb(1.00, 0.25, 0.00)
local TURBO_COL_EDGE2   = rgb(1.00, 0.25, 0.00)
local TURBO_COL_MID     = rgb(1.00, 0.25, 0.00)
local TURBO_SPLIT1      = 0.50
local TURBO_SPLIT2      = 0.50
local TURBO_DRAW_OUTLINE= true

-- Outline helpers
local function outlineOil(box)
  if not DRAW_OUTLINE then return end
  rect(box.x,   box.y-1, box.w,1, OUTLINE_COL)
  rect(box.x,   box.y+box.h, box.w,1, OUTLINE_COL)
  rect(box.x-1, box.y-1, 1,box.h+2, OUTLINE_COL)
  rect(box.x+box.w, box.y-1, 1,box.h+2, OUTLINE_COL)
end
local function outlineTurbo(box)
  if not TURBO_DRAW_OUTLINE then return end
  rect(box.x,   box.y-1, box.w,1, OUTLINE_COL)
  rect(box.x,   box.y+box.h, box.w,1, OUTLINE_COL)
  rect(box.x-1, box.y-1, 1,box.h+2, OUTLINE_COL)
  rect(box.x+box.w, box.y-1, 1,box.h+2, OUTLINE_COL)
end

-- Color ramps by fraction
local function colorAtFracOil(frac)
  if frac <= SPLIT1 then return COL_EDGE1
  elseif frac <= SPLIT2 then return COL_EDGE2
  else return COL_MID end
end
local function colorAtFracTurbo(frac)
  if frac <= TURBO_SPLIT1 then return TURBO_COL_EDGE1
  elseif frac <= TURBO_SPLIT2 then return TURBO_COL_EDGE2
  else return TURBO_COL_MID end
end

-- Draw N boxes across a span
local function drawBoxesSpan(x0, wSpan, boxes, gap, fromLeft, lit, height, colorByIndex, baseY)
  if boxes <= 0 or wSpan <= 0 then return end
  local totalGap = gap * (boxes - 1)
  local boxW = (wSpan - totalGap) / boxes
  if boxW <= 0 then return end

  for i = 1, boxes do
    if i <= lit then
      local x = fromLeft
        and (x0 + (i-1) * (boxW + gap))
        or  (x0 + wSpan - i * boxW - (i-1) * gap)
      rect(x, baseY, boxW, height, colorByIndex(i, boxes))
    end
  end
end

-- Configurable discrete bar renderer
local function drawBarDiscrete(box, mode, boxesHalf, boxesTotal, gapPx, progress, colorFunc, drawOutlineFunc)
  progress = clamp(progress or 0, 0, 1)

  if mode == "both" then
    local halfW   = box.w * 0.5
    local litHalf = math.floor(progress * boxesHalf + 1e-6)
    drawBoxesSpan(box.x,         halfW, boxesHalf, gapPx, true,  litHalf, box.h,
      function(i, n) return colorFunc((i-0.5)/n) end, box.y)
    drawBoxesSpan(box.x + halfW, halfW, boxesHalf, gapPx, false, litHalf, box.h,
      function(i, n) return colorFunc((i-0.5)/n) end, box.y)
  else
    local lit = math.floor(progress * boxesTotal + 1e-6)
    drawBoxesSpan(box.x, box.w, boxesTotal, gapPx, mode=="left", lit, box.h,
      function(i, n) return colorFunc((i-0.5)/n) end, box.y)
  end

  if drawOutlineFunc then drawOutlineFunc(box) end
end

-- Optional full red fill (if you enable RED_MODE)
local function drawFullRedBoxes(box, mode, boxesHalf, boxesTotal, gapPx)
  if mode == "both" then
    local halfW = box.w * 0.5
    drawBoxesSpan(box.x,         halfW, boxesHalf, gapPx, true,  boxesHalf, box.h,
      function() return RED_COLOR end, box.y)
    drawBoxesSpan(box.x + halfW, halfW, boxesHalf, gapPx, false, boxesHalf, box.h,
      function() return RED_COLOR end, box.y)
  else
    drawBoxesSpan(box.x, box.w, boxesTotal, gapPx, mode=="left", boxesTotal, box.h,
      function() return RED_COLOR end, box.y)
  end
  outlineOil(box)
end

-- Oil & Turbo sources
local function getOilC()
  local t = (car and (car.oilTemperature or car.oilTemp)) or 0
  return t
end
local function getTurboFrac()
  local f = 0
  if car then
    if car.turboBoost ~= nil then
      f = car.turboBoost
    elseif car.boostRatio ~= nil then
      f = car.boostRatio
    elseif car.boostPressure ~= nil and car.maxBoostPressure ~= nil then
      f = (car.boostPressure) / math.max(0.001, car.maxBoostPressure)
    end
  end
  return clamp(f, 0, 1)
end

-- Runtime state for blink/red behavior (kept for compatibility)
local blinkTimer = 0
local demoRpm = 4000
local demoState = "ramp"  -- "ramp" → "hold"
local DEBUG_PLAY  = false
local DEBUG_SPEED = 1000
local DEBUG_HOLD  = 1.0
local DEBUG_MODE  = false
local DEBUG_RPM   = 6400

--══════════════════════════════════════════════════════════════════════════════
--                                   UPDATE
--══════════════════════════════════════════════════════════════════════════════
local function cluster_update(dt)
  -- Live values
  local speedKmh = car.speedKmh
  local speedVal = useMph and (speedKmh / 1.609344) or speedKmh

  local timeText   = string.format("%02d:%02d", sim.timeHours, sim.timeMinutes)
  local timeText2  = string.format("%02d:%02d", sim.timeHours, sim.timeMinutes)
  local speedText  = tostring(math.floor(speedVal))
  local tempText   = string.format("%.1f", sim.ambientTemperature)
  local gearText   = gearT[car.gear]
  local gearText2  = gearM[car.gear]
  local rangeText  = tostring(math.floor(car.fuel * fuelKmsPerLitre))
  local totalText  = string.format("%1d", car.distanceDrivenTotalKm)
  local sessText   = string.format("%1d", car.distanceDrivenSessionKm)
  local waterTemp  = string.format("%1d", car.waterTemperature)

  -- ⚙️ DRIVE ICON (shows only in gear 1–8)
  local driveSize = 51
  local drivePos  = vec2(1082, 1007)
  local gear      = car.gear or 0
  if gear >= 1 and gear <= 8 then
    ui.drawImage("./cluster/gear_1.png",
      drivePos, vec2(drivePos.x + driveSize, drivePos.y + driveSize), rgbm(1, 1, 1, 1))
  end

  -- 🕒 TIME
  display.text({ text = timeText, pos = v("1452.5, 996.6"), letter = v("32.5, 75"),
    font = "/fonts/gallardo_1", color = white, alignment = 0.9, width = 0, spacing = -1 })

  -- 🕒 TIME2
  display.text({ text = timeText2, pos = v("770.8, 1286"), letter = v("20, 40"),
    font = "/fonts/gallardo_1", color = white, alignment = 0.5, width = 250, spacing = -1 })

  -- 🗓 CALENDAR (date)
  local dtbl = (ac and ac.getDateTime and ac.getDateTime()) or os.date("*t")
  display.text({
    text = string.format("%02d/%02d/%04d", dtbl.month, dtbl.day, dtbl.year),
    pos = v("1282, 1124"),
    letter = v("25, 50"),
    font = "/fonts/audi_vln_hd",
    color = white,
    alignment = 1,
    width = 350,
    spacing=-2.5
  })

  -- 🗓 DAY OF WEEK
  local dayText
  if type(dtbl) == "table" then
    if dtbl.wday then
      local days = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
      dayText = days[dtbl.wday]
    elseif dtbl.weekday then
      local daysMon0 = {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"}
      dayText = daysMon0[(dtbl.weekday % 7) + 1]
    end
    if not dayText and dtbl.year and dtbl.month and dtbl.day then
      local w = os.date("*t", os.time{year=dtbl.year, month=dtbl.month, day=dtbl.day, hour=12}).wday
      local days = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
      dayText = days[w]
    end
  end
  dayText = dayText or os.date("%A")
  display.text({ text = dayText,  pos = v("1354, 1073"), letter = v("25, 50"),
    font = "/fonts/audi_vln_hd", color = white, alignment = 0.5, width = 350,spacing=-1.75 })

  -- 🚗 SPEED
  display.text({ text = speedText, pos = v("891, 1167.9"), letter = v("65,65"),
    font = "/fonts/aventador_b", color = white, alignment = .5, width = 255, spacing = -15 })

  -- 📏 TOTAL KM
  display.text({ text = totalText, pos = v("785.8, 1247.5"), letter = v("20, 40"),
    font = "/fonts/gallardo_1", color = white, alignment = 0.5, width = 250, spacing =0 })

  -- 📏 SESSION KM
  display.text({ text = sessText, pos = v("1060.8, 1247.5"), letter = v("20, 40"),
    font = "/fonts/gallardo_1", color = white, alignment = 0.5, width = 250, spacing = -1 })

  -- 🌡 TEMP (ambient, with decimal)
  local tempValue = sim.ambientTemperature
  local intPart, decPart = string.match(string.format("%.1f", tempValue), "(%d+)%.(%d)")
  display.text({ text = intPart, pos = v("815.3, 1286"), letter = v("20, 40"),
    font = "/fonts/gallardo_1", color = white, alignment = 1, width = 350 })
  display.text({ text = "." .. decPart, pos = v("845.3, 1286"), letter = v("20, 40"),
    font = "/fonts/gallardo_1", color = white, alignment = 1, width = 350, spacing = -5 })

  -- 🛣 RANGE
  display.text({ text = rangeText, pos = v("1322, 834"), letter = v("20, 40"),
    font = "/fonts/gallardo_1", color = white, alignment = 1, width = 155, spacing = -1 })

-- ⚙️ GEAR (black fill + orange outline)
local gearPos = v("985.2, 1009")
local gearLetter = v("110, 110")
local gearFont = "/fonts/aventador_b"
local gearColorFill = rgb(0, 0, 0)          -- black fill
local gearColorOutline = rgb(1.0, 0.5, 0.25)   -- orange outline

-- draw outline (offset in 8 directions)
local outlineOffsets = {
  vec2(-4, 0), vec2(4, 0), vec2(0, -4), vec2(0, 4),
  vec2(-4, -4), vec2(-4, 2), vec2(4, -4), vec2(4, 4)
}
for _, off in ipairs(outlineOffsets) do
  display.text({
    text = gearText,
    pos = gearPos + off,
    letter = gearLetter,
    font = gearFont,
    color = gearColorOutline,
    alignment = 0.5,
    width = 0,
    spacing = 1
  })
end

-- draw center fill
display.text({
  text = gearText,
  pos = gearPos,
  letter = gearLetter,
  font = gearFont,
  color = gearColorFill,
  alignment = 0.5,
  width = 0,
  spacing = 1
})
  -- ⚙️ WATER TEMPERATURE
  display.text({ text = waterTemp, pos = v("409.9, 1255.7"), letter = v("16, 34"),
    font = "/fonts/gallardo_1", color = white, alignment = 0.5, width = 250, spacing = 0 })

  -- Hide PARK symbol when handbrake released
  if car.handbrake < 0.1 then
    display.rect({ pos = vec2(1894.2, 1306.6), size = vec2(85.0, 85.0),
      color = rgb(0,0,0), corner = 0, border = 0, opacity = 1 })
  end

  -- low-beam symbol 

    if car.lowBeams and car.headlightsActive then
        local pos = vec2(720.7, 1280)
        ui.drawImage("./cluster/low_beam_headlight.png", pos,vec2(pos.x+40, pos.y+40) , rgbm(0.1,1,0.1,1))
    end

    if  car.highBeams and car.headlightsActive then
        local pos = vec2(720.7, 1280)
        ui.drawImage("./cluster/low_beam_headlight.png", pos,vec2(pos.x+40, pos.y+40) , rgbm(0.1,1,0.1,1))
    end

-- high-beam symbol
    if car.highBeams then
        local pos = vec2(670.7, 1280)
        ui.drawImage("./cluster/headlight.png", pos,vec2(pos.x+40, pos.y+40) , rgbm(0.0,0.4,1,1))
    end

  -- tc symbol off 
    if car.tractionControlMode == 0 or tractionControlInAction then
        local pos = vec2(770.7, 1285)
        ui.drawImage("./cluster/traction_controle.png", pos,vec2(pos.x+40, pos.y+40) , rgbm(25,2,0,1))
    end

  -- Turn signals (blink)
  local iconSize = 60
  local turnL    = vec2(610, 380)
  local screenW  = (ui.windowSize and ui.windowSize().x) or 1920
  local turnR    = vec2(screenW - turnL.x - iconSize, turnL.y)


  if car.turningLeftLights or car.turningRightLights then
    setInterval(function() indicatorActive = not indicatorActive end, 0.334, "indicatorActive")
  else
    clearInterval("indicatorActive")
    indicatorActive = false
  end

  if car.turningLeftOnly and indicatorActive then
    ui.beginRotation()
    ui.drawImage("./cluster/turn_signal.png",
      turnL, vec2(turnL.x + iconSize, turnL.y + iconSize), rgbm(0.1, 1, 0.1, 1))
    ui.endRotation(-90)
  elseif car.turningRightOnly and indicatorActive then
    ui.drawImage("./cluster/turn_signal.png",
      turnR, vec2(turnR.x + iconSize, turnR.y + iconSize), rgbm(0.1, 1, 0.1, 1))
  elseif car.hazardLights and indicatorActive then
    ui.drawImage("./cluster/turn_signal.png",
      turnR, vec2(turnR.x + iconSize, turnR.y + iconSize), rgbm(0.1, 1, 0.1, 1))
    ui.beginRotation()
    ui.drawImage("./cluster/turn_signal.png",
      turnL, vec2(turnL.x + iconSize, turnL.y + iconSize), rgbm(0.1, 1, 0.1, 1))
    ui.endRotation(-90)
  end

  --═══════════════════════ DRAW: Masked alpha bar (fuel by default) ══════════
  --═══════════════════════ STEP LED STRIPS (Water + Fuel) ════════════════════
  -- WATER: 50→130°C across 8 LEDs
  local waterC = car.waterTemperature or 0
  local waterFrac = clamp((waterC - 50) / math.max(1, (130 - 50)), 0, 1)
  local waterLit = ledsFromFraction(waterFrac, #WATER_LED_RECTS)
  drawStepLeds(WATER_LED_RECTS, waterLit, rgb(0,0,0), 1, 3)

  -- FUEL: 0→100% across 8 LEDs
  local fuelFrac = 0
  if car and car.maxFuel and car.maxFuel > 0 then
    fuelFrac = clamp((car.fuel or 0) / car.maxFuel, 0, 1)
  end
  local fuelLit = ledsFromFraction(fuelFrac, #FUEL_LED_RECTS)
  drawStepLeds(FUEL_LED_RECTS, fuelLit, rgb(0,0,0), 1, 3)

  if ENABLE_MASKED_ALPHA_BAR then
    drawMaskedAlphaFillBar(BAR_POS, BAR_SIZE, alphaBarFraction(), BAR_FADE)
    debugDrawMask()
  end

  --═══════════════════════ DRAW: Oil temp bar (discrete boxes) ═══════════════
  blinkTimer = blinkTimer + (type(dt)=="number" and dt or 0)

  local oilC = getOilC()
  local oilProgress = (oilC - OIL_MIN_C) / math.max(1, (OIL_MAX_C - OIL_MIN_C))
  oilProgress = clamp(oilProgress, 0, 1)

  if RED_MODE ~= "off" then
    local rpm
    if DEBUG_PLAY then
      if demoState == "ramp" then
        demoRpm = demoRpm + DEBUG_SPEED * dt
        if demoRpm >= RED_RPM then
          demoRpm = RED_RPM
          demoState = "hold"
          demoHoldT = 0
        end
      elseif demoState == "hold" then
        demoHoldT = (demoHoldT or 0) + dt
        if demoHoldT >= DEBUG_HOLD then
          demoRpm = 4000
          demoState = "ramp"
        end
      end
      rpm = demoRpm
    else
      rpm = DEBUG_MODE and DEBUG_RPM or (car.rpm or 0)
    end

    if rpm >= RED_RPM then
      if RED_MODE == "solid" then
        drawFullRedBoxes(BAR_OIL, FILL_MODE_OIL, BOXES_PER_HALF, BOXES_TOTAL, BOX_GAP_PX)
      else
        local period = 1 / math.max(0.1, BLINK_HZ)
        local on = (blinkTimer % period) < (period * 0.5)
        if     RED_MODE == "blink" then
          if on then drawFullRedBoxes(BAR_OIL, FILL_MODE_OIL, BOXES_PER_HALF, BOXES_TOTAL, BOX_GAP_PX)
          else       drawBarDiscrete(BAR_OIL, FILL_MODE_OIL, BOXES_PER_HALF, BOXES_TOTAL, BOX_GAP_PX,
                                     oilProgress, colorAtFracOil, outlineOil) end
        elseif RED_MODE == "blink_hide" then
          if on then drawFullRedBoxes(BAR_OIL, FILL_MODE_OIL, BOXES_PER_HALF, BOXES_TOTAL, BOX_GAP_PX)
          else outlineOil(BAR_OIL) end
        end
      end
    else
      drawBarDiscrete(BAR_OIL, FILL_MODE_OIL, BOXES_PER_HALF, BOXES_TOTAL, BOX_GAP_PX,
                      oilProgress, colorAtFracOil, outlineOil)
    end
  else
    drawBarDiscrete(BAR_OIL, FILL_MODE_OIL, BOXES_PER_HALF, BOXES_TOTAL, BOX_GAP_PX,
                    oilProgress, colorAtFracOil, outlineOil)
  end

  --═══════════════════════ DRAW: Turbo spool bar (discrete boxes) ════════════
  local turboF = getTurboFrac()
  drawBarDiscrete(
    BAR_TURBO,
    TURBO_FILL_MODE,
    TURBO_BOXES_HALF,
    TURBO_BOXES_TOTAL,
    TURBO_GAP_PX,
    turboF,
    colorAtFracTurbo,
    outlineTurbo
  )
end


-- g_force_poly_custom.lua — G-meter with custom polygon clamp + textured dot

local DOT_TEXTURE = "cluster/g_force_marker.png"
local DOT_SIZE    = vec2(250, 250)

local CENTER = vec2(532, 1053.2)

local POLY = {
  vec2(484.6, 1008.1),  -- front-left
  vec2(417.8, 1053.4),  -- left
  vec2(459.8, 1127.2),  -- rear-left
  vec2(606.4, 1126.7),  -- rear-right
  vec2(642.7, 1053.9),  -- right
  vec2(575.1, 1008.6),  -- front-right
}

local ANGLE_OFFSET = math.pi * 0
local MAX_G = 2.0
local LAG = 0.92
local EMA_HZ = 7.5

local SHOW_GRID  =false
local GRID_ALPHA = 0.25
local RINGS      = 5

GF_SmoothedAccel = { x = 0, z = 0 }
local function v2mix(a,b,t) return vec2(a.x+(b.x-a.x)*t, a.y+(b.y-a.y)*t) end

local function gf_dotLine(a,b,opacity)
  local dx, dy = b.x-a.x, b.y-a.y
  local len = math.sqrt(dx*dx+dy*dy)
  local step = 2
  local n = math.max(1, math.floor(len / step))
  for i=0,n do
    local t = i/n
    local x = a.x + dx*t
    local y = a.y + dy*t
    display.rect({ pos=vec2(x-1, y-1), size=vec2(2,2), color=rgb(1,1,1), opacity=opacity or GRID_ALPHA, corner=1 })
  end
end

local function raySegmentIntersect(center, D, A, B)
  local Cx, Cy = center.x, center.y
  local Dx, Dy = D.x, D.y
  local Ax, Ay = A.x, A.y
  local Bx, By = B.x, B.y
  local sx, sy = Bx - Ax, By - Ay
  local det = -Dx*sy + Dy*sx
  if math.abs(det) < 1e-6 then return nil end
  local rx = Ax - Cx; local ry = Ay - Cy
  local t = (-sy*rx + sx*ry) / det
  if t < 0 then return nil end
  local u = (-Dy*rx + Dx*ry) / det
  if u < 0 or u > 1 then return nil end
  return t
end

local function rayToPolygon(center, angle, poly)
  local D = vec2(math.cos(angle), math.sin(angle))
  local bestT, hit = nil, nil
  for i=1,#poly do
    local A = poly[i]
    local B = poly[(i % #poly) + 1]
    local t = raySegmentIntersect(center, D, A, B)
    if t and (not bestT or t < bestT) then
      bestT = t
      hit = vec2(center.x + D.x * t, center.y + D.y * t)
    end
  end
  return hit
end

local function ema(prev, target, hz, dt)
  dt = dt or (sim and sim.dt) or 1/60
  if hz <= 0 then return target end
  local k = math.exp(-hz * dt)
  return target + (prev - target) * k
end

local function gforce_update(dt)
  if math and math.applyLag and sim and sim.dt then
    GF_SmoothedAccel.x = math.applyLag(GF_SmoothedAccel.x, car.acceleration.x or 0, LAG, sim.dt)
    GF_SmoothedAccel.z = math.applyLag(GF_SmoothedAccel.z, car.acceleration.z or 0, LAG, sim.dt)
  else
    GF_SmoothedAccel.x = ema(GF_SmoothedAccel.x, car.acceleration.x or 0, EMA_HZ, dt)
    GF_SmoothedAccel.z = ema(GF_SmoothedAccel.z, car.acceleration.z or 0, EMA_HZ, dt)
  end

  local gx, gz = GF_SmoothedAccel.x, GF_SmoothedAccel.z
  local ang = math.atan2(gz, gx) + ANGLE_OFFSET
  local mag = math.sqrt(gx*gx + gz*gz) / math.max(0.001, MAX_G)
  mag = clamp(mag, 0, 1)

  local hit = rayToPolygon(CENTER, ang, POLY)
  local pos = hit and v2mix(CENTER, hit, mag) or CENTER

  if SHOW_GRID then
    for i=1,#POLY do gf_dotLine(POLY[i], POLY[(i % #POLY)+1], GRID_ALPHA) end
    for i=1,#POLY do gf_dotLine(CENTER, POLY[i], GRID_ALPHA) end
    for r=1,RINGS-1 do
      local t = r / RINGS
      local first, prev = nil, nil
      for i=1,#POLY do
        local p = v2mix(CENTER, POLY[i], t)
        if not first then first = p end
        if prev then gf_dotLine(prev, p, GRID_ALPHA) end
        prev = p
        if i==#POLY then gf_dotLine(p, first, GRID_ALPHA) end
      end
    end
  end

  display.image({ image = DOT_TEXTURE, pos = vec2(pos.x - DOT_SIZE.x*0.5, pos.y - DOT_SIZE.y*0.5), size = DOT_SIZE })

  display.text({ text = string.format("%.1f", math.max(GF_SmoothedAccel.x, 0)),pos = vec2(653.9, 1035.9),
    letter = vec2(22, 44), font = "/fonts/audi_vln_hd", width = 46, alignment = 1, spacing = -10, color = rgbm(1, 1, 1, 1) })
  display.text({ text = string.format("%.1f", math.max(-GF_SmoothedAccel.x, 0)),pos = vec2(365, 1035.9),
    letter = vec2(22, 44), font = "/fonts/audi_vln_hd", width = 46, alignment = 1, spacing = -10, color = rgbm(1, 1, 1, 1) })
  display.text({ text = string.format("%.1f", math.max(-GF_SmoothedAccel.z, 0)), pos = vec2(509.7, 973.1),
    letter = vec2(22, 44), font = "/fonts/audi_vln_hd", width = 46, alignment = 1, spacing = -10, color = rgbm(1, 1, 1, 1) })
  display.text({ text = string.format("%.1f", math.max(GF_SmoothedAccel.z, 0)), pos = vec2(509.7, 1135.5),
    letter = vec2(22, 44), font = "/fonts/audi_vln_hd", width = 46, alignment = 1, spacing = -10, color = rgbm(1, 1, 1, 1) })
end


function update(dt)
  cluster_update(dt)
  gforce_update(dt)
end
