feat: enhance billboard rendering with pie chart and storage ring display

This commit is contained in:
MayaTheShy
2026-03-26 14:44:51 -04:00
parent 664741504f
commit 38ff3f61bd

View File

@@ -1450,8 +1450,8 @@ end
-------------------------------------------------
-- Billboard rendering (raw monitor API, no Opus UI)
-- Read-only goals display: storage bar, top items
-- chart, stock alerts, activity indicators.
-- Visual goals display with pie chart, storage
-- gauge, stock alerts, and activity indicators.
-------------------------------------------------
local BB = {} -- billboard color theme
@@ -1470,10 +1470,24 @@ BB.alertLow = colors.red
BB.alertWarn = colors.orange
BB.activityOn = colors.lime
BB.activityOff = colors.gray
BB.graphBar = colors.cyan
BB.graphBarAlt = colors.lightBlue
BB.sectionHead = colors.yellow
-- Pie chart slice colors (12 distinct CC colors)
local PIE_COLORS = {
colors.red,
colors.orange,
colors.yellow,
colors.lime,
colors.green,
colors.cyan,
colors.lightBlue,
colors.blue,
colors.purple,
colors.magenta,
colors.pink,
colors.brown,
}
local function bbFormatNumber(n)
if n >= 1000000 then
return string.format("%.1fM", n / 1000000)
@@ -1509,24 +1523,17 @@ local function bbClearLine(y, bg)
D.billboardMon.write(string.rep(" ", bbW))
end
local function bbWriteCentered(y, text, fg, bg)
bbClearLine(y, bg or BB.bg)
bbSetColors(fg or BB.value, bg or BB.bg)
D.billboardMon.setCursorPos(math.floor((bbW - #text) / 2) + 1, y)
D.billboardMon.write(text)
end
local function bbWriteAt(x, y, text, fg, bg)
bbSetColors(fg or BB.value, bg or BB.bg)
D.billboardMon.setCursorPos(x, y)
D.billboardMon.write(text)
end
local function bbHLine(y, char, fg, bg)
bbClearLine(y, bg or BB.bg)
bbSetColors(fg or BB.border, bg or BB.bg)
local function bbHLine(y, fg)
bbClearLine(y)
bbSetColors(fg or BB.border, BB.bg)
D.billboardMon.setCursorPos(1, y)
D.billboardMon.write(string.rep(char or "-", bbW))
D.billboardMon.write(string.rep("\x8c", bbW))
end
local function bbDrawBar(x, y, width, filled, fgColor, bgColor)
@@ -1541,52 +1548,171 @@ local function bbDrawBar(x, y, width, filled, fgColor, bgColor)
D.billboardMon.setBackgroundColor(BB.bg)
end
--- Draw a pie chart using colored character cells.
--- slices = { { fraction=0.0-1.0, color=colors.X }, ... }
--- Draws into the rectangle (x1,y1) to (x1+size-1, y1+rows-1)
--- size = width in chars, rows = height in chars
local function bbDrawPie(x1, y1, size, rows, slices)
local cx = size / 2 -- center in char coords
local cy = rows / 2
local radius = math.min(cx, cy) - 0.5
-- Character cells are ~1.5x taller than wide; squish Y
local aspect = 1.5
-- Build cumulative angle boundaries
local angles = {}
local cumulative = 0
for i, slice in ipairs(slices) do
angles[i] = { start = cumulative, stop = cumulative + slice.fraction, color = slice.color }
cumulative = cumulative + slice.fraction
end
for row = 0, rows - 1 do
D.billboardMon.setCursorPos(x1, y1 + row)
for col = 0, size - 1 do
local dx = (col + 0.5 - cx)
local dy = (row + 0.5 - cy) * aspect
local dist = math.sqrt(dx * dx + dy * dy)
if dist <= radius then
-- Compute angle 0-1 (0 = top, clockwise)
local angle = math.atan2(dx, -dy) -- top = 0, clockwise
if angle < 0 then angle = angle + 2 * math.pi end
local frac = angle / (2 * math.pi)
-- Find which slice
local cellColor = BB.bg
for _, s in ipairs(angles) do
if frac >= s.start and frac < s.stop then
cellColor = s.color
break
end
end
-- Catch rounding at the very end
if cellColor == BB.bg and #angles > 0 then
cellColor = angles[#angles].color
end
D.billboardMon.setBackgroundColor(cellColor)
D.billboardMon.write(" ")
else
D.billboardMon.setBackgroundColor(BB.bg)
D.billboardMon.write(" ")
end
end
end
D.billboardMon.setBackgroundColor(BB.bg)
end
--- Draw a storage ring/donut gauge.
--- Draws a ring showing used vs free with percentage in center.
local function bbDrawStorageRing(x1, y1, size, rows)
local cx = size / 2
local cy = rows / 2
local outerR = math.min(cx, cy) - 0.5
local innerR = outerR * 0.55
local aspect = 1.5
local ratio = cache.usedRatio or 0
local usedColor = BB.barFull
if ratio > 0.9 then usedColor = BB.barCrit
elseif ratio > 0.75 then usedColor = BB.barWarn end
for row = 0, rows - 1 do
D.billboardMon.setCursorPos(x1, y1 + row)
for col = 0, size - 1 do
local dx = (col + 0.5 - cx)
local dy = (row + 0.5 - cy) * aspect
local dist = math.sqrt(dx * dx + dy * dy)
if dist <= outerR and dist >= innerR then
-- In the ring — determine angle (top = 0, clockwise)
local angle = math.atan2(dx, -dy)
if angle < 0 then angle = angle + 2 * math.pi end
local frac = angle / (2 * math.pi)
if frac < ratio then
D.billboardMon.setBackgroundColor(usedColor)
else
D.billboardMon.setBackgroundColor(BB.barEmpty)
end
D.billboardMon.write(" ")
else
D.billboardMon.setBackgroundColor(BB.bg)
D.billboardMon.write(" ")
end
end
end
-- Write percentage in center of ring
local pct = tostring(math.floor(ratio * 100 + 0.5)) .. "%"
local textY = y1 + math.floor(cy)
local textX = x1 + math.floor(cx - #pct / 2)
bbWriteAt(textX, textY, pct, usedColor)
end
-- Billboard section: header
local function bbDrawHeader(y)
bbClearLine(y, BB.headerBg)
bbSetColors(BB.headerFg, BB.headerBg)
local title = " INVENTORY GOALS BILLBOARD "
local title = " INVENTORY BILLBOARD "
D.billboardMon.setCursorPos(math.floor((bbW - #title) / 2) + 1, y)
D.billboardMon.write(title)
return y + 1
end
-- Billboard section: storage capacity
local function bbDrawStorage(y)
-- Billboard section: storage ring + stats (side by side)
local function bbDrawStorageSection(y)
bbHLine(y)
y = y + 1
bbWriteAt(2, y, "> STORAGE", BB.sectionHead)
y = y + 1
-- Ring takes up a square area
local ringSize = math.min(math.floor(bbW * 0.35), bbH - y - 6)
if ringSize < 5 then ringSize = 5 end
local ringRows = math.floor(ringSize / 1.5) -- aspect correction
if ringRows < 3 then ringRows = 3 end
-- Draw ring on the left
bbDrawStorageRing(2, y, ringSize, ringRows)
-- Stats text to the right of the ring
local statsX = ringSize + 4
local statsY = y + 1
bbWriteAt(statsX, statsY, "STORAGE", BB.sectionHead)
statsY = statsY + 1
local ratio = cache.usedRatio or 0
local pct = math.floor(ratio * 100 + 0.5)
local barColor = BB.barFull
if ratio > 0.9 then barColor = BB.barCrit
elseif ratio > 0.75 then barColor = BB.barWarn end
local usedColor = BB.barFull
if ratio > 0.9 then usedColor = BB.barCrit
elseif ratio > 0.75 then usedColor = BB.barWarn end
local barW = bbW - 10
if barW < 10 then barW = 10 end
bbWriteAt(2, y, bbPadLeft(pct .. "%", 4), barColor)
bbDrawBar(7, y, barW, ratio, barColor, BB.barEmpty)
y = y + 1
bbWriteAt(statsX, statsY, string.format("%s / %s slots",
bbFormatNumber(cache.usedSlots), bbFormatNumber(cache.totalSlots)), BB.value)
statsY = statsY + 1
local stats = string.format(
"Slots: %s/%s | Items: %s | Chests: %d",
bbFormatNumber(cache.usedSlots),
bbFormatNumber(cache.totalSlots),
bbFormatNumber(cache.grandTotal),
cache.chestCount
)
bbWriteAt(2, y, stats, BB.label)
y = y + 1
return y
bbWriteAt(statsX, statsY, string.format("%s total items",
bbFormatNumber(cache.grandTotal)), BB.label)
statsY = statsY + 1
bbWriteAt(statsX, statsY, string.format("%d chests", cache.chestCount), BB.label)
statsY = statsY + 1
-- Mini capacity bar
local barW = bbW - statsX - 1
if barW > 3 then
bbWriteAt(statsX, statsY, "", BB.label)
bbDrawBar(statsX, statsY, barW, ratio, usedColor, BB.barEmpty)
end
return y + ringRows + 1
end
-- Billboard section: top items bar chart
local function bbDrawItems(y, maxRows)
-- Billboard section: pie chart + legend
local function bbDrawPieSection(y, maxH)
bbHLine(y)
y = y + 1
bbWriteAt(2, y, "> TOP ITEMS", BB.sectionHead)
bbWriteAt(2, y, "ITEM DISTRIBUTION", BB.sectionHead)
y = y + 1
state.ensureItemList()
@@ -1596,49 +1722,96 @@ local function bbDrawItems(y, maxRows)
return y + 1
end
-- Sort and pick top N for pie slices
local sorted = {}
for i, item in ipairs(items) do sorted[i] = item end
table.sort(sorted, function(a, b) return a.total > b.total end)
local count = math.min(#sorted, maxRows, cfg.BILLBOARD_TOP_ITEMS or 20)
if count < 1 then count = 1 end
local maxVal = sorted[1].total
if maxVal < 1 then maxVal = 1 end
local numW = 8
local nameW = math.floor(bbW * 0.35)
if nameW < 12 then nameW = 12 end
if nameW > 25 then nameW = 25 end
local barW = bbW - nameW - numW - 5
if barW < 5 then barW = 5 end
for i = 1, count do
local item = sorted[i]
local name = shortName(item.name)
local num = bbFormatNumber(item.total)
local frac = item.total / maxVal
bbClearLine(y)
bbWriteAt(1, y, bbPadLeft(tostring(i), 2), BB.border)
bbWriteAt(4, y, bbPadRight(name, nameW), BB.value)
local barColor = (i % 2 == 0) and BB.graphBarAlt or BB.graphBar
local barStart = 4 + nameW + 1
bbDrawBar(barStart, y, barW, frac, barColor, BB.barEmpty)
bbWriteAt(barStart + barW + 1, y, bbPadLeft(num, numW), BB.value)
y = y + 1
local maxSlices = math.min(#PIE_COLORS, cfg.BILLBOARD_TOP_ITEMS or 12, #sorted)
local topTotal = 0
for i = 1, maxSlices do
topTotal = topTotal + sorted[i].total
end
return y
-- "Other" bucket for remaining items
local otherTotal = (cache.grandTotal or 0) - topTotal
local total = cache.grandTotal or 1
if total < 1 then total = 1 end
-- Build slices
local slices = {}
local legendItems = {}
for i = 1, maxSlices do
local frac = sorted[i].total / total
if frac < 0.005 then break end -- skip tiny slices
table.insert(slices, { fraction = frac, color = PIE_COLORS[i] })
table.insert(legendItems, {
name = shortName(sorted[i].name),
count = sorted[i].total,
pct = math.floor(frac * 100 + 0.5),
color = PIE_COLORS[i],
})
end
if otherTotal > 0 then
table.insert(slices, { fraction = otherTotal / total, color = BB.border })
table.insert(legendItems, {
name = "Other",
count = otherTotal,
pct = math.floor(otherTotal / total * 100 + 0.5),
color = BB.border,
})
end
-- Layout: pie on left, legend on right
local pieSize = math.min(math.floor(bbW * 0.4), maxH - 1)
if pieSize < 5 then pieSize = 5 end
local pieRows = math.floor(pieSize / 1.5)
if pieRows < 3 then pieRows = 3 end
if pieRows > maxH - 1 then pieRows = maxH - 1 end
-- Draw pie
if #slices > 0 then
bbDrawPie(2, y, pieSize, pieRows, slices)
end
-- Draw legend to the right
local legX = pieSize + 4
local legY = y
local legW = bbW - legX - 1
for i, item in ipairs(legendItems) do
if legY >= y + pieRows then break end
if legW < 10 then break end
-- Color swatch
D.billboardMon.setCursorPos(legX, legY)
D.billboardMon.setBackgroundColor(item.color)
D.billboardMon.write(" ")
D.billboardMon.setBackgroundColor(BB.bg)
D.billboardMon.write(" ")
-- Name + count
local label = item.name
local countStr = bbFormatNumber(item.count)
local pctStr = item.pct .. "%"
local infoW = legW - 4 -- 2 swatch + 1 space + padding
local detail = string.format("%s %s", pctStr, countStr)
local nameW = infoW - #detail - 1
if nameW < 4 then nameW = 4 end
if #label > nameW then label = label:sub(1, nameW - 1) .. "." end
bbWriteAt(legX + 3, legY, bbPadRight(label, nameW), BB.value)
bbWriteAt(legX + 3 + nameW + 1, legY, detail, BB.label)
legY = legY + 1
end
return y + pieRows
end
-- Billboard section: stock alerts
-- Billboard section: stock alerts (compact)
local function bbDrawAlerts(y, maxRows)
bbHLine(y)
y = y + 1
bbWriteAt(2, y, "> STOCK ALERTS", BB.sectionHead)
y = y + 1
local alerts = state.activeAlerts
if not alerts or #alerts == 0 then
@@ -1646,13 +1819,18 @@ local function bbDrawAlerts(y, maxRows)
return y + 1
end
bbWriteAt(2, y, "ALERTS", BB.sectionHead)
local countStr = string.format("(%d)", #alerts)
bbWriteAt(9, y, countStr, BB.alertWarn)
y = y + 1
local colW = math.floor(bbW / 2)
local twoCol = (bbW >= 40)
local twoCol = (bbW >= 30)
local row = 0
for i, alert in ipairs(alerts) do
if row >= maxRows then
bbWriteAt(2, y, string.format(" ... +%d more", #alerts - i + 1), BB.alertWarn)
bbWriteAt(2, y, string.format(" +%d more", #alerts - i + 1), BB.alertWarn)
y = y + 1
break
end
@@ -1662,14 +1840,8 @@ local function bbDrawAlerts(y, maxRows)
local minVal = alert.min or 0
local ratio = minVal > 0 and (current / minVal) or 1
local icon, color
if ratio < 0.5 then
icon, color = "!", BB.alertLow
else
icon, color = "!", BB.alertWarn
end
local text = string.format("%s %s: %s/%s", icon, label, bbFormatNumber(current), bbFormatNumber(minVal))
local color = ratio < 0.5 and BB.alertLow or BB.alertWarn
local text = string.format("! %s %s/%s", label, bbFormatNumber(current), bbFormatNumber(minVal))
if twoCol then
local col = ((i - 1) % 2 == 0) and 2 or (colW + 1)
@@ -1689,43 +1861,45 @@ local function bbDrawAlerts(y, maxRows)
return y
end
-- Billboard section: activity footer
local function bbDrawActivity(y)
bbHLine(y)
y = y + 1
bbClearLine(y)
bbWriteAt(2, y, "ACTIVITY:", BB.sectionHead)
-- Billboard section: activity bar (single line)
local function bbDrawActivityBar(y)
bbClearLine(y, BB.headerBg)
bbSetColors(BB.headerFg, BB.headerBg)
local labels = {
{ key = "sorting", label = "SORT" },
{ key = "scanning", label = "SCAN" },
{ key = "smelting", label = "SMELT" },
{ key = "dispensing", label = "DISPENSE" },
{ key = "defragging", label = "DEFRAG" },
{ key = "composting", label = "COMPOST" },
{ key = "crafting", label = "CRAFT" },
{ key = "autocrafting", label = "AUTOCRAFT" },
{ key = "discarding", label = "DISCARD" },
{ key = "smelting", label = "SMLT" },
{ key = "dispensing", label = "DISP" },
{ key = "defragging", label = "DEFR" },
{ key = "composting", label = "COMP" },
{ key = "crafting", label = "CRFT" },
{ key = "autocrafting", label = "AUTO" },
{ key = "discarding", label = "DISC" },
}
local x = 12
local anyActive = false
local parts = {}
for _, entry in ipairs(labels) do
if activity[entry.key] then
anyActive = true
if x + #entry.label + 2 > bbW then
y = y + 1
bbClearLine(y)
x = 3
end
bbWriteAt(x, y, entry.label, BB.activityOn)
x = x + #entry.label + 2
table.insert(parts, entry.label)
end
end
if not anyActive then
bbWriteAt(12, y, "IDLE", BB.activityOff)
local text
if #parts > 0 then
text = " " .. table.concat(parts, " | ") .. " "
else
text = " IDLE "
end
D.billboardMon.setCursorPos(math.floor((bbW - #text) / 2) + 1, y)
if #parts > 0 then
bbSetColors(colors.white, colors.green)
else
bbSetColors(BB.label, BB.headerBg)
end
D.billboardMon.write(text)
return y + 1
end
@@ -1738,29 +1912,34 @@ function D.drawBillboard()
D.billboardMon.clear()
local y = 1
-- Header bar
y = bbDrawHeader(y)
y = bbDrawStorage(y)
-- Allocate vertical space: alerts ~3-8 lines, footer ~2 lines
local alertLines = state.activeAlerts and #state.activeAlerts or 0
local alertSectionH = math.max(3, math.min(6, math.ceil(alertLines / 2) + 2))
local footerH = 2
local itemRows = bbH - y - alertSectionH - footerH
if itemRows < 3 then itemRows = 3 end
-- Storage ring + stats
y = bbDrawStorageSection(y)
y = bbDrawItems(y, itemRows)
-- Pie chart + legend (gets remaining space minus alerts + footer)
local alertCount = state.activeAlerts and #state.activeAlerts or 0
local alertH = math.max(2, math.min(5, math.ceil(alertCount / 2) + 2))
local footerH = 1 -- activity bar
local pieH = bbH - y - alertH - footerH
if pieH < 5 then pieH = 5 end
y = bbDrawPieSection(y, pieH)
local alertMaxRows = bbH - y - footerH
if alertMaxRows < 2 then alertMaxRows = 2 end
-- Alerts
local alertMaxRows = bbH - y - footerH - 1
if alertMaxRows < 1 then alertMaxRows = 1 end
y = bbDrawAlerts(y, alertMaxRows)
-- Fill gap before footer
local footerY = bbH - 1
while y < footerY do
while y < bbH do
bbClearLine(y)
y = y + 1
end
bbDrawActivity(footerY)
-- Activity bar at very bottom
bbDrawActivityBar(bbH)
end
return D