feat: implement billboard monitor support and remove legacy code

This commit is contained in:
MayaTheShy
2026-03-26 14:16:23 -04:00
parent c1b1713699
commit 8a50bc586d
6 changed files with 378 additions and 627 deletions

View File

@@ -26,6 +26,8 @@ D.mon = nil
D.monName = nil
D.smelterMon = nil
D.smelterMonName = nil
D.billboardMon = nil
D.billboardMonName = nil
-- Opus UI devices and pages
local mainDevice = nil
@@ -160,6 +162,29 @@ function D.setupSmelterMonitor()
return true
end
function D.setupBillboardMonitor()
if not cfg.BILLBOARD_MONITOR_SIDE or cfg.BILLBOARD_MONITOR_SIDE == "" then
return false
end
local mon = peripheral.wrap(cfg.BILLBOARD_MONITOR_SIDE)
if mon and mon.setTextScale then
D.billboardMon = mon
D.billboardMonName = cfg.BILLBOARD_MONITOR_SIDE
D.billboardMon.setTextScale(0.5)
return true
end
-- Fallback: try to find a monitor with that name on the network
for _, name in ipairs(peripheral.getNames()) do
if name == cfg.BILLBOARD_MONITOR_SIDE and peripheral.getType(name) == "monitor" then
D.billboardMon = peripheral.wrap(name)
D.billboardMonName = name
D.billboardMon.setTextScale(0.5)
return true
end
end
return false
end
-------------------------------------------------
-- Build main dashboard page
-------------------------------------------------
@@ -1416,6 +1441,321 @@ function D.handleSmelterTouch(x, y)
end
end
-------------------------------------------------
-- Billboard rendering (raw monitor API, no Opus UI)
-- Read-only goals display: storage bar, top items
-- chart, stock alerts, activity indicators.
-------------------------------------------------
local BB = {} -- billboard color theme
BB.bg = colors.black
BB.headerBg = colors.blue
BB.headerFg = colors.white
BB.border = colors.gray
BB.label = colors.lightGray
BB.value = colors.white
BB.barFull = colors.lime
BB.barEmpty = colors.gray
BB.barWarn = colors.yellow
BB.barCrit = colors.red
BB.alertOk = colors.lime
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
local function bbFormatNumber(n)
if n >= 1000000 then
return string.format("%.1fM", n / 1000000)
elseif n >= 10000 then
return string.format("%.1fK", n / 1000)
elseif n >= 1000 then
return string.format("%d,%03d", math.floor(n / 1000), n % 1000)
end
return tostring(n)
end
local function bbPadRight(s, w)
if #s >= w then return s:sub(1, w) end
return s .. string.rep(" ", w - #s)
end
local function bbPadLeft(s, w)
if #s >= w then return s:sub(1, w) end
return string.rep(" ", w - #s) .. s
end
-- Billboard drawing primitives (operate on D.billboardMon)
local bbW, bbH = 0, 0
local function bbSetColors(fg, bg)
D.billboardMon.setTextColor(fg)
D.billboardMon.setBackgroundColor(bg)
end
local function bbClearLine(y, bg)
D.billboardMon.setCursorPos(1, y)
D.billboardMon.setBackgroundColor(bg or BB.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)
D.billboardMon.setCursorPos(1, y)
D.billboardMon.write(string.rep(char or "-", bbW))
end
local function bbDrawBar(x, y, width, filled, fgColor, bgColor)
local fillW = math.floor(filled * width + 0.5)
if fillW > width then fillW = width end
if fillW < 0 then fillW = 0 end
D.billboardMon.setCursorPos(x, y)
D.billboardMon.setBackgroundColor(fgColor or BB.barFull)
D.billboardMon.write(string.rep(" ", fillW))
D.billboardMon.setBackgroundColor(bgColor or BB.barEmpty)
D.billboardMon.write(string.rep(" ", width - fillW))
D.billboardMon.setBackgroundColor(BB.bg)
end
-- Billboard section: header
local function bbDrawHeader(y)
bbClearLine(y, BB.headerBg)
bbSetColors(BB.headerFg, BB.headerBg)
local title = " INVENTORY GOALS 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)
bbHLine(y)
y = y + 1
bbWriteAt(2, y, "> STORAGE", BB.sectionHead)
y = y + 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 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
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
end
-- Billboard section: top items bar chart
local function bbDrawItems(y, maxRows)
bbHLine(y)
y = y + 1
bbWriteAt(2, y, "> TOP ITEMS", BB.sectionHead)
y = y + 1
state.ensureItemList()
local items = cache.itemList
if not items or #items == 0 then
bbWriteAt(2, y, "No items in storage", BB.label)
return y + 1
end
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
end
return y
end
-- Billboard section: stock alerts
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
bbWriteAt(2, y, "* All stocks OK", BB.alertOk)
return y + 1
end
local colW = math.floor(bbW / 2)
local twoCol = (bbW >= 40)
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)
y = y + 1
break
end
local label = alert.label or shortName(alert.name or "?")
local current = alert.current or 0
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))
if twoCol then
local col = ((i - 1) % 2 == 0) and 2 or (colW + 1)
if col == 2 then bbClearLine(y) end
bbWriteAt(col, y, bbPadRight(text, colW - 1), color)
if (i - 1) % 2 == 1 or i == #alerts then
y = y + 1
row = row + 1
end
else
bbClearLine(y)
bbWriteAt(2, y, text, color)
y = y + 1
row = row + 1
end
end
return y
end
-- Billboard section: activity footer
local function bbDrawActivity(y)
bbHLine(y)
y = y + 1
bbClearLine(y)
bbWriteAt(2, y, "ACTIVITY:", BB.sectionHead)
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" },
}
local x = 12
local anyActive = false
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
end
end
if not anyActive then
bbWriteAt(12, y, "IDLE", BB.activityOff)
end
return y + 1
end
-- Main billboard draw entry point
function D.drawBillboard()
if not D.billboardMon then return end
bbW, bbH = D.billboardMon.getSize()
D.billboardMon.setBackgroundColor(BB.bg)
D.billboardMon.clear()
local y = 1
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
y = bbDrawItems(y, itemRows)
local alertMaxRows = bbH - y - footerH
if alertMaxRows < 2 then alertMaxRows = 2 end
y = bbDrawAlerts(y, alertMaxRows)
-- Fill gap before footer
local footerY = bbH - 1
while y < footerY do
bbClearLine(y)
y = y + 1
end
bbDrawActivity(footerY)
end
return D
end