- Implemented main dashboard UI with item selection, search functionality, and pagination. - Added smelter dashboard with furnace status, crafting options, and recipe management. - Introduced touch handlers for user interactions including item ordering and keyboard input. - Integrated drawing helpers for UI elements and status messages. - Established state management for UI updates and interactions.
999 lines
37 KiB
Lua
999 lines
37 KiB
Lua
-- manager/display.lua — Dashboard rendering and touch handlers
|
|
-- Usage: local display = dofile("manager/display.lua")(ctx)
|
|
|
|
return function(ctx)
|
|
|
|
local cfg = ctx.cfg
|
|
local state = ctx.state
|
|
local log = ctx.log
|
|
local ui = ctx.ui
|
|
local ops = ctx.ops
|
|
|
|
local cache = state.cache
|
|
local activity = state.activity
|
|
|
|
local D = {}
|
|
|
|
-------------------------------------------------
|
|
-- Monitor handles (set during init)
|
|
-------------------------------------------------
|
|
|
|
D.mon = nil
|
|
D.monName = nil
|
|
D.smelterMon = nil
|
|
D.smelterMonName = nil
|
|
|
|
function D.setupMonitor()
|
|
D.mon, D.monName = ui.setupMonitor(cfg.MONITOR_SIDE, cfg.SMELTER_MONITOR_SIDE)
|
|
return D.mon ~= nil
|
|
end
|
|
|
|
function D.setupSmelterMonitor()
|
|
D.smelterMon, D.smelterMonName = ui.setupSmelterMonitor(cfg.SMELTER_MONITOR_SIDE, D.monName)
|
|
return D.smelterMon ~= nil
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Main dashboard UI state
|
|
-------------------------------------------------
|
|
|
|
local selectedAmount = 1
|
|
local amountOptions = {1, 4, 8, 16, 32, 64}
|
|
|
|
local touchZones = {}
|
|
local pendingZones = {}
|
|
|
|
local currentPage = 1
|
|
local totalPages = 1
|
|
local searchQuery = ""
|
|
local showKeyboard = false
|
|
|
|
local kbRows = {
|
|
{"Q","W","E","R","T","Y","U","I","O","P"},
|
|
{"A","S","D","F","G","H","J","K","L"},
|
|
{"Z","X","C","V","B","N","M"},
|
|
}
|
|
|
|
-------------------------------------------------
|
|
-- Smelter dashboard UI state
|
|
-------------------------------------------------
|
|
|
|
local smelterView = "status"
|
|
local smelterPage = 1
|
|
local smelterTotalPages = 1
|
|
local smelterTouchZones = {}
|
|
local smelterPendingZones = {}
|
|
|
|
-------------------------------------------------
|
|
-- Drawing helpers (delegated to shared ui module)
|
|
-------------------------------------------------
|
|
|
|
local draw = nil
|
|
|
|
local function setDrawTarget(target)
|
|
draw = target
|
|
ui.draw = target
|
|
end
|
|
|
|
local function monWrite(x, y, text, fg, bg) ui.monWrite(x, y, text, fg, bg) end
|
|
local function monFill(y, color) ui.monFill(y, color) end
|
|
local function monCenter(y, text, fg, bg) ui.monCenter(y, text, fg, bg) end
|
|
local function monBar(x, y, w, r, bc, bgc) ui.monBar(x, y, w, r, bc, bgc) end
|
|
local function drawButton(x, y, t, fg, bg, pl, pr) return ui.drawButton(x, y, t, fg, bg, pl, pr) end
|
|
|
|
local function addZone(x1, y1, x2, y2, action, data)
|
|
ui.addZone(pendingZones, x1, y1, x2, y2, action, data)
|
|
end
|
|
|
|
local function hitTest(x, y)
|
|
return ui.hitTest(touchZones, x, y)
|
|
end
|
|
|
|
local function addSmelterZone(x1, y1, x2, y2, action, data)
|
|
ui.addZone(smelterPendingZones, x1, y1, x2, y2, action, data)
|
|
end
|
|
|
|
local function smelterHitTest(x, y)
|
|
return ui.hitTest(smelterTouchZones, x, y)
|
|
end
|
|
|
|
local function getFilteredItems()
|
|
state.ensureItemList()
|
|
return ui.getFilteredItems(cache.itemList, searchQuery)
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Main dashboard drawing
|
|
-------------------------------------------------
|
|
|
|
function D.drawDashboard()
|
|
if not D.mon then return end
|
|
|
|
local w, h = D.mon.getSize()
|
|
pendingZones = {}
|
|
|
|
setDrawTarget(window.create(D.mon, 1, 1, w, h, false))
|
|
draw.setBackgroundColor(colors.black)
|
|
draw.clear()
|
|
|
|
-- Title bar
|
|
monFill(1, colors.blue)
|
|
monCenter(1, " ** INVENTORY MANAGER ** ", colors.white, colors.blue)
|
|
|
|
-- Status bar
|
|
monFill(2, colors.gray)
|
|
local statusParts = {}
|
|
table.insert(statusParts, string.format(" Chests: %d", cache.chestCount))
|
|
table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --")
|
|
table.insert(statusParts, cache.barrelOk and "Barrel: OK" or "Barrel: --")
|
|
if cache.furnaceCount and cache.furnaceCount > 0 then
|
|
table.insert(statusParts, string.format("Furnaces: %d", cache.furnaceCount))
|
|
end
|
|
|
|
local actParts = {}
|
|
if activity.sorting then table.insert(actParts, "SORTING") end
|
|
if activity.dispensing then table.insert(actParts, "DISPENSING") end
|
|
if activity.smelting then table.insert(actParts, "SMELTING") end
|
|
if activity.scanning then table.insert(actParts, "SCANNING") end
|
|
if activity.defragging then table.insert(actParts, "DEFRAG") end
|
|
if activity.composting then table.insert(actParts, "COMPOST") end
|
|
|
|
monWrite(2, 2, table.concat(statusParts, " | "), colors.white, colors.gray)
|
|
|
|
if #actParts > 0 then
|
|
local actStr = " " .. table.concat(actParts, " | ") .. " "
|
|
monWrite(w - #actStr, 2, actStr, colors.white, colors.orange)
|
|
end
|
|
|
|
-- Divider
|
|
monFill(3, colors.lightBlue)
|
|
monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue)
|
|
|
|
-- Storage capacity
|
|
monFill(4, colors.black)
|
|
local capLabel = string.format(" Storage: %d/%d slots (%d free)",
|
|
cache.usedSlots, cache.totalSlots, cache.freeSlots)
|
|
monWrite(2, 4, capLabel, colors.lightGray, colors.black)
|
|
|
|
local barStart = #capLabel + 4
|
|
local barWidth = w - barStart - 2
|
|
if barWidth > 4 then
|
|
local barColor = colors.lime
|
|
if cache.usedRatio > 0.9 then barColor = colors.red
|
|
elseif cache.usedRatio > 0.7 then barColor = colors.orange
|
|
elseif cache.usedRatio > 0.5 then barColor = colors.yellow
|
|
end
|
|
monBar(barStart, 4, barWidth, cache.usedRatio, barColor, colors.gray)
|
|
local pctStr = string.format(" %d%% ", math.floor(cache.usedRatio * 100))
|
|
local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2)
|
|
monWrite(pctX, 4, pctStr, colors.white, barColor)
|
|
end
|
|
|
|
-- Amount selector (row 5)
|
|
monFill(5, colors.black)
|
|
monWrite(2, 5, "Qty:", colors.lightGray, colors.black)
|
|
local btnX = 7
|
|
for _, amt in ipairs(amountOptions) do
|
|
local label = tostring(amt)
|
|
local bg = (amt == selectedAmount) and colors.cyan or colors.gray
|
|
local fg = (amt == selectedAmount) and colors.white or colors.lightGray
|
|
local x1, y1, x2, y2 = drawButton(btnX, 5, label, fg, bg)
|
|
addZone(x1, y1, x2, y2, "amount", amt)
|
|
btnX = x2 + 2
|
|
end
|
|
|
|
local refreshBg = activity.scanning and colors.yellow or colors.green
|
|
local refreshFg = activity.scanning and colors.black or colors.white
|
|
local refreshTxt = activity.scanning and "Scanning" or "Refresh"
|
|
local scanX = w - #refreshTxt - 3
|
|
local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, refreshTxt, refreshFg, refreshBg, 1, 1)
|
|
addZone(sx1, sy1, sx2, sy2, "scan", nil)
|
|
|
|
-- Search bar + Pagination (row 6)
|
|
monFill(6, colors.black)
|
|
|
|
local kbLabel = showKeyboard and " X " or " ? "
|
|
local kbBg = showKeyboard and colors.red or colors.purple
|
|
monWrite(2, 6, kbLabel, colors.white, kbBg)
|
|
addZone(2, 6, 4, 6, "kb_toggle", nil)
|
|
|
|
local queryDisplay = searchQuery
|
|
if showKeyboard then
|
|
queryDisplay = queryDisplay .. "|"
|
|
elseif queryDisplay == "" then
|
|
queryDisplay = "search..."
|
|
end
|
|
local fieldW = math.floor(w * 0.4)
|
|
if fieldW < 10 then fieldW = 10 end
|
|
local displayText = queryDisplay:sub(1, fieldW)
|
|
displayText = displayText .. string.rep("_", math.max(0, fieldW - #displayText))
|
|
monWrite(6, 6, displayText,
|
|
(searchQuery == "" and not showKeyboard) and colors.gray or colors.white,
|
|
colors.black)
|
|
addZone(6, 6, 5 + fieldW, 6, "kb_toggle", nil)
|
|
|
|
local filteredItems = getFilteredItems()
|
|
|
|
local maxRows = h - 10
|
|
if maxRows < 1 then maxRows = 1 end
|
|
totalPages = math.max(1, math.ceil(#filteredItems / maxRows))
|
|
if currentPage > totalPages then currentPage = totalPages end
|
|
if currentPage < 1 then currentPage = 1 end
|
|
|
|
local pageStr = string.format("Pg %d/%d", currentPage, totalPages)
|
|
local navW = 3 + 1 + #pageStr + 1 + 3
|
|
local navX = w - navW
|
|
|
|
if currentPage > 1 then
|
|
monWrite(navX, 6, " < ", colors.white, colors.gray)
|
|
addZone(navX, 6, navX + 2, 6, "page_prev", nil)
|
|
else
|
|
monWrite(navX, 6, " < ", colors.lightGray, colors.black)
|
|
end
|
|
|
|
monWrite(navX + 4, 6, pageStr, colors.lightGray, colors.black)
|
|
|
|
local nextX = navX + 4 + #pageStr + 1
|
|
if currentPage < totalPages then
|
|
monWrite(nextX, 6, " > ", colors.white, colors.gray)
|
|
addZone(nextX, 6, nextX + 2, 6, "page_next", nil)
|
|
else
|
|
monWrite(nextX, 6, " > ", colors.lightGray, colors.black)
|
|
end
|
|
|
|
-- Column headers (row 7)
|
|
local row = 7
|
|
monFill(row, colors.gray)
|
|
monWrite(2, row, "#", colors.lightGray, colors.gray)
|
|
monWrite(5, row, "Item", colors.lightGray, colors.gray)
|
|
monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray)
|
|
monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray)
|
|
monWrite(w - 1, row, ">", colors.lightGray, colors.gray)
|
|
row = row + 1
|
|
|
|
-- Item rows
|
|
local maxCount = 0
|
|
for _, item in ipairs(filteredItems) do
|
|
if item.total > maxCount then maxCount = item.total end
|
|
end
|
|
if maxCount == 0 then maxCount = 1 end
|
|
|
|
local startIdx = (currentPage - 1) * maxRows + 1
|
|
local endIdx = math.min(startIdx + maxRows - 1, #filteredItems)
|
|
|
|
if #filteredItems == 0 then
|
|
monFill(8, colors.black)
|
|
monFill(9, colors.black)
|
|
if searchQuery ~= "" then
|
|
monCenter(9, "No items match \"" .. searchQuery .. "\"", colors.gray, colors.black)
|
|
else
|
|
monCenter(9, "No items in storage", colors.gray, colors.black)
|
|
end
|
|
row = 10
|
|
else
|
|
for i = startIdx, endIdx do
|
|
local item = filteredItems[i]
|
|
local y = row
|
|
local short = item.name:gsub("^minecraft:", ""):gsub("_", " ")
|
|
short = short:sub(1,1):upper() .. short:sub(2)
|
|
|
|
local maxNameLen = w - 30
|
|
if #short > maxNameLen then
|
|
short = short:sub(1, maxNameLen - 2) .. ".."
|
|
end
|
|
|
|
local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray
|
|
monFill(y, rowBg)
|
|
|
|
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
|
|
monWrite(5, y, short, colors.white, rowBg)
|
|
monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg)
|
|
|
|
local ratio = item.total / maxCount
|
|
local barColor = colors.lime
|
|
if ratio < 0.25 then barColor = colors.red
|
|
elseif ratio < 0.5 then barColor = colors.orange
|
|
end
|
|
monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray)
|
|
|
|
monWrite(w - 1, y, ">", colors.orange, rowBg)
|
|
addZone(1, y, w, y, "order", item.name)
|
|
|
|
row = row + 1
|
|
end
|
|
end
|
|
|
|
local lastItemRow = h - 3
|
|
while row <= lastItemRow do
|
|
monFill(row, colors.black)
|
|
row = row + 1
|
|
end
|
|
|
|
if showKeyboard then
|
|
local keyW = 3
|
|
local keyGap = 1
|
|
|
|
local kbDefs = {
|
|
{ keys = kbRows[1], specials = {{ label = " Bksp ", action = "kb_bksp", bg = colors.red }} },
|
|
{ keys = kbRows[2], specials = {{ label = " Done ", action = "kb_done", bg = colors.green }} },
|
|
{ keys = kbRows[3], specials = {
|
|
{ label = " Space ", action = "kb_space", bg = colors.lightGray },
|
|
{ label = " Clr ", action = "kb_clear", bg = colors.orange },
|
|
}},
|
|
}
|
|
|
|
for rowIdx, def in ipairs(kbDefs) do
|
|
local y = h - 3 + rowIdx
|
|
monFill(y, colors.black)
|
|
|
|
local keysW = #def.keys * keyW + math.max(0, #def.keys - 1) * keyGap
|
|
local specialsW = 0
|
|
for _, sp in ipairs(def.specials) do
|
|
specialsW = specialsW + keyGap + #sp.label
|
|
end
|
|
local rowW = keysW + specialsW
|
|
local x = math.floor((w - rowW) / 2) + 1
|
|
|
|
for ki, key in ipairs(def.keys) do
|
|
monWrite(x, y, " " .. key .. " ", colors.white, colors.gray)
|
|
addZone(x, y, x + keyW - 1, y, "kb_key", key:lower())
|
|
x = x + keyW
|
|
if ki < #def.keys then x = x + keyGap end
|
|
end
|
|
|
|
for _, sp in ipairs(def.specials) do
|
|
x = x + keyGap
|
|
monWrite(x, y, sp.label, colors.white, sp.bg)
|
|
addZone(x, y, x + #sp.label - 1, y, sp.action, nil)
|
|
x = x + #sp.label
|
|
end
|
|
end
|
|
else
|
|
-- Status message
|
|
monFill(h - 2, colors.black)
|
|
if #state.activeAlerts > 0 then
|
|
local alertIdx = math.floor(os.epoch("utc") / 2000) % #state.activeAlerts + 1
|
|
local a = state.activeAlerts[alertIdx]
|
|
local alertMsg = string.format(" LOW STOCK: %s (%d/%d) ", a.label, a.current, a.min)
|
|
monCenter(h - 2, alertMsg, colors.white, colors.red)
|
|
elseif state.statusTimer > 0 and #state.statusMessage > 0 then
|
|
monCenter(h - 2, state.statusMessage, state.statusColor, colors.black)
|
|
end
|
|
|
|
-- Footer
|
|
state.ensureItemList()
|
|
monFill(h - 1, colors.gray)
|
|
local footerLeft = string.format(" Total: %d items | %d types ",
|
|
cache.grandTotal, #cache.itemList)
|
|
monWrite(2, h - 1, footerLeft, colors.white, colors.gray)
|
|
|
|
if searchQuery ~= "" then
|
|
local filterNote = string.format("| Showing %d ", #filteredItems)
|
|
monWrite(2 + #footerLeft + 1, h - 1, filterNote, colors.yellow, colors.gray)
|
|
end
|
|
|
|
local timeStr = textutils.formatTime(os.time(), true)
|
|
monWrite(w - #timeStr - 1, h - 1, timeStr, colors.lightGray, colors.gray)
|
|
|
|
-- Bottom accent
|
|
monFill(h, colors.blue)
|
|
local bottomMsg = " Tap item to order "
|
|
if activity.dispensing then
|
|
bottomMsg = " DISPENSING... "
|
|
elseif activity.smelting then
|
|
bottomMsg = " SMELTING... "
|
|
elseif activity.sorting then
|
|
bottomMsg = " SORTING BARREL... "
|
|
elseif activity.defragging then
|
|
bottomMsg = " DEFRAGMENTING... "
|
|
elseif activity.composting then
|
|
bottomMsg = " COMPOSTING... "
|
|
end
|
|
monCenter(h, bottomMsg, colors.lightBlue, colors.blue)
|
|
end
|
|
|
|
draw.setVisible(true)
|
|
touchZones = pendingZones
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Smelter dashboard drawing
|
|
-------------------------------------------------
|
|
|
|
function D.drawSmelterDashboard()
|
|
if not D.smelterMon then return end
|
|
|
|
local w, h = D.smelterMon.getSize()
|
|
smelterPendingZones = {}
|
|
|
|
setDrawTarget(window.create(D.smelterMon, 1, 1, w, h, false))
|
|
draw.setBackgroundColor(colors.black)
|
|
draw.clear()
|
|
|
|
-- Title bar
|
|
monFill(1, colors.purple)
|
|
monCenter(1, " ** SMELTER DASHBOARD ** ", colors.white, colors.purple)
|
|
|
|
-- Status bar
|
|
monFill(2, colors.gray)
|
|
local activeCount = 0
|
|
for _, fs in ipairs(cache.furnaceStatus or {}) do
|
|
if fs.active then activeCount = activeCount + 1 end
|
|
end
|
|
local statusStr = string.format(" Furnaces: %d Active: %d",
|
|
cache.furnaceCount or 0, activeCount)
|
|
monWrite(2, 2, statusStr, colors.white, colors.gray)
|
|
|
|
local pauseLabel = state.smeltingPaused and " PAUSED " or " ACTIVE "
|
|
local pauseBg = state.smeltingPaused and colors.red or colors.lime
|
|
local pauseFg = state.smeltingPaused and colors.white or colors.black
|
|
monWrite(w - #pauseLabel, 2, pauseLabel, pauseFg, pauseBg)
|
|
addSmelterZone(w - #pauseLabel, 2, w - 1, 2, "toggle_pause", nil)
|
|
|
|
-- Divider
|
|
monFill(3, colors.magenta)
|
|
monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.pink, colors.magenta)
|
|
|
|
-- Tab row
|
|
monFill(4, colors.black)
|
|
local tabStatusBg = smelterView == "status" and colors.purple or colors.gray
|
|
local tabSmeltBg = smelterView == "smelt" and colors.purple or colors.gray
|
|
local tabCraftBg = smelterView == "craft" and colors.purple or colors.gray
|
|
local tabMissingBg = smelterView == "missing" and colors.purple or colors.gray
|
|
local bx1, by1, bx2, by2
|
|
bx1, by1, bx2, by2 = drawButton(2, 4, "Status", colors.white, tabStatusBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "status")
|
|
bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Smelt", colors.white, tabSmeltBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "smelt")
|
|
bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Craft", colors.white, tabCraftBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "craft")
|
|
bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Missing", colors.white, tabMissingBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "missing")
|
|
|
|
local craftAvailCount = nil
|
|
|
|
if smelterView == "status" then
|
|
-- Furnace Status View
|
|
monFill(5, colors.gray)
|
|
local outCol = math.floor(w * 0.40)
|
|
local fuelCol = math.floor(w * 0.65)
|
|
local statCol = w - 6
|
|
monWrite(2, 5, "#", colors.lightGray, colors.gray)
|
|
monWrite(4, 5, "T", colors.lightGray, colors.gray)
|
|
monWrite(6, 5, "Input", colors.lightGray, colors.gray)
|
|
monWrite(outCol, 5, "Output", colors.lightGray, colors.gray)
|
|
monWrite(fuelCol, 5, "Fuel", colors.lightGray, colors.gray)
|
|
monWrite(statCol, 5, "State", colors.lightGray, colors.gray)
|
|
|
|
local furnaceList = cache.furnaceStatus or {}
|
|
local maxRows = h - 8
|
|
if maxRows < 1 then maxRows = 1 end
|
|
smelterTotalPages = math.max(1, math.ceil(#furnaceList / maxRows))
|
|
if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end
|
|
if smelterPage < 1 then smelterPage = 1 end
|
|
|
|
local startIdx = (smelterPage - 1) * maxRows + 1
|
|
local endIdx = math.min(startIdx + maxRows - 1, #furnaceList)
|
|
|
|
local row = 6
|
|
if #furnaceList == 0 then
|
|
monFill(7, colors.black)
|
|
monCenter(7, "No furnaces found on network", colors.gray, colors.black)
|
|
row = 8
|
|
else
|
|
for i = startIdx, endIdx do
|
|
local fs = furnaceList[i]
|
|
local y = row
|
|
local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray
|
|
monFill(y, rowBg)
|
|
|
|
monWrite(2, y, string.format("%d", i), colors.lightBlue, rowBg)
|
|
|
|
local typeAbbr = "F"
|
|
local typeColor = colors.orange
|
|
if fs.type == "minecraft:smoker" then
|
|
typeAbbr = "S"
|
|
typeColor = colors.green
|
|
elseif fs.type == "minecraft:blast_furnace" then
|
|
typeAbbr = "B"
|
|
typeColor = colors.cyan
|
|
end
|
|
monWrite(4, y, typeAbbr, typeColor, rowBg)
|
|
|
|
if fs.input then
|
|
local inName = fs.input.name:gsub("^minecraft:", ""):gsub("_", " ")
|
|
local maxIn = outCol - 8
|
|
if #inName > maxIn then inName = inName:sub(1, maxIn - 2) .. ".." end
|
|
monWrite(6, y, inName, colors.white, rowBg)
|
|
monWrite(outCol - 4, y, "x" .. fs.input.count, colors.yellow, rowBg)
|
|
else
|
|
monWrite(6, y, "(empty)", colors.lightGray, rowBg)
|
|
end
|
|
|
|
if fs.output then
|
|
local outName = fs.output.name:gsub("^minecraft:", ""):gsub("_", " ")
|
|
local maxOut = fuelCol - outCol - 5
|
|
if #outName > maxOut then outName = outName:sub(1, maxOut - 2) .. ".." end
|
|
monWrite(outCol, y, outName, colors.white, rowBg)
|
|
monWrite(fuelCol - 4, y, "x" .. fs.output.count, colors.yellow, rowBg)
|
|
else
|
|
monWrite(outCol, y, "-", colors.lightGray, rowBg)
|
|
end
|
|
|
|
if fs.fuel then
|
|
local fuelName = fs.fuel.name:gsub("^minecraft:", ""):gsub("_", " ")
|
|
local maxFuel = statCol - fuelCol - 4
|
|
if #fuelName > maxFuel then fuelName = fuelName:sub(1, maxFuel - 2) .. ".." end
|
|
monWrite(fuelCol, y, fuelName, colors.white, rowBg)
|
|
monWrite(statCol - 4, y, "x" .. fs.fuel.count, colors.yellow, rowBg)
|
|
else
|
|
monWrite(fuelCol, y, "-", colors.lightGray, rowBg)
|
|
end
|
|
|
|
if state.smeltingPaused then
|
|
monWrite(statCol, y, "PAUSE", colors.red, rowBg)
|
|
elseif fs.active then
|
|
monWrite(statCol, y, " COOK", colors.lime, rowBg)
|
|
elseif fs.input and not fs.fuel then
|
|
monWrite(statCol, y, "FUEL?", colors.orange, rowBg)
|
|
else
|
|
monWrite(statCol, y, " IDLE", colors.lightGray, rowBg)
|
|
end
|
|
|
|
row = row + 1
|
|
end
|
|
end
|
|
|
|
while row <= h - 2 do monFill(row, colors.black); row = row + 1 end
|
|
|
|
elseif smelterView == "smelt" then
|
|
-- Smelt Recipe Manager View
|
|
local recipeList = {}
|
|
for inputName, recipe in pairs(cfg.SMELTABLE) do
|
|
local short = inputName:gsub("^minecraft:", ""):gsub("_", " ")
|
|
short = short:sub(1,1):upper() .. short:sub(2)
|
|
local resultShort = recipe.result:gsub("^minecraft:", ""):gsub("_", " ")
|
|
resultShort = resultShort:sub(1,1):upper() .. resultShort:sub(2)
|
|
local types = ""
|
|
if recipe.furnaceSet["minecraft:furnace"] then types = types .. "F" end
|
|
if recipe.furnaceSet["minecraft:smoker"] then types = types .. "S" end
|
|
if recipe.furnaceSet["minecraft:blast_furnace"] then types = types .. "B" end
|
|
local enabled = not state.disabledRecipes[inputName]
|
|
local inStorage = 0
|
|
if cache.catalogue[inputName] then
|
|
for _, s in ipairs(cache.catalogue[inputName]) do
|
|
inStorage = inStorage + s.total
|
|
end
|
|
end
|
|
table.insert(recipeList, {
|
|
inputName = inputName,
|
|
inputShort = short,
|
|
resultShort = resultShort,
|
|
types = types,
|
|
enabled = enabled,
|
|
inStorage = inStorage,
|
|
})
|
|
end
|
|
table.sort(recipeList, function(a, b) return a.inputShort < b.inputShort end)
|
|
|
|
local arrowCol = math.floor(w * 0.30)
|
|
local typeCol = math.floor(w * 0.60)
|
|
local stockCol = math.floor(w * 0.72)
|
|
local toggleCol = w - 5
|
|
|
|
monFill(5, colors.gray)
|
|
monWrite(2, 5, "Input", colors.lightGray, colors.gray)
|
|
monWrite(arrowCol, 5, "Output", colors.lightGray, colors.gray)
|
|
monWrite(typeCol, 5, "Type", colors.lightGray, colors.gray)
|
|
monWrite(stockCol, 5, "Stock", colors.lightGray, colors.gray)
|
|
monWrite(toggleCol, 5, "On?", colors.lightGray, colors.gray)
|
|
|
|
local bulkX = w - 22
|
|
bx1, by1, bx2, by2 = drawButton(bulkX, 4, "All On", colors.white, colors.green)
|
|
addSmelterZone(bx1, by1, bx2, by2, "enable_all", nil)
|
|
bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "All Off", colors.white, colors.red)
|
|
addSmelterZone(bx1, by1, bx2, by2, "disable_all", nil)
|
|
|
|
local maxRows = h - 8
|
|
if maxRows < 1 then maxRows = 1 end
|
|
smelterTotalPages = math.max(1, math.ceil(#recipeList / maxRows))
|
|
if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end
|
|
if smelterPage < 1 then smelterPage = 1 end
|
|
|
|
local startIdx = (smelterPage - 1) * maxRows + 1
|
|
local endIdx = math.min(startIdx + maxRows - 1, #recipeList)
|
|
|
|
local row = 6
|
|
for i = startIdx, endIdx do
|
|
local r = recipeList[i]
|
|
local y = row
|
|
local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray
|
|
monFill(y, rowBg)
|
|
|
|
local maxInputLen = arrowCol - 3
|
|
local inputDisplay = r.inputShort
|
|
if #inputDisplay > maxInputLen then
|
|
inputDisplay = inputDisplay:sub(1, maxInputLen - 2) .. ".."
|
|
end
|
|
monWrite(2, y, inputDisplay, colors.white, rowBg)
|
|
|
|
local maxOutLen = typeCol - arrowCol - 2
|
|
local outDisplay = r.resultShort
|
|
if #outDisplay > maxOutLen then
|
|
outDisplay = outDisplay:sub(1, maxOutLen - 2) .. ".."
|
|
end
|
|
monWrite(arrowCol, y, outDisplay, colors.lightBlue, rowBg)
|
|
|
|
monWrite(typeCol, y, r.types, colors.orange, rowBg)
|
|
monWrite(stockCol, y, tostring(r.inStorage), colors.yellow, rowBg)
|
|
|
|
if r.enabled then
|
|
monWrite(toggleCol, y, " ON ", colors.white, colors.green)
|
|
else
|
|
monWrite(toggleCol, y, " OFF", colors.white, colors.red)
|
|
end
|
|
addSmelterZone(1, y, w, y, "toggle_recipe", r.inputName)
|
|
|
|
row = row + 1
|
|
end
|
|
|
|
while row <= h - 2 do monFill(row, colors.black); row = row + 1 end
|
|
|
|
elseif smelterView == "craft" then
|
|
-- Available Crafting Recipes
|
|
local turtleOk = ctx.craftTurtleName and peripheral.isPresent(ctx.craftTurtleName)
|
|
local tLabel = turtleOk and " Turtle OK " or " No Turtle "
|
|
local tBg = turtleOk and colors.lime or colors.red
|
|
local tFg = turtleOk and colors.black or colors.white
|
|
monWrite(w - #tLabel, 4, tLabel, tFg, tBg)
|
|
|
|
local availList = {}
|
|
for idx, recipe in ipairs(cfg.CRAFTABLE) do
|
|
if ops.canCraftRecipe(recipe) then
|
|
local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ")
|
|
short = short:sub(1,1):upper() .. short:sub(2)
|
|
local batches = ops.maxCraftBatches(recipe)
|
|
table.insert(availList, {
|
|
idx = idx,
|
|
short = short,
|
|
count = recipe.count,
|
|
batches = batches,
|
|
})
|
|
end
|
|
end
|
|
craftAvailCount = #availList
|
|
|
|
monFill(5, colors.gray)
|
|
local makeCol = w - 6
|
|
monWrite(2, 5, "#", colors.lightGray, colors.gray)
|
|
monWrite(4, 5, "Output", colors.lightGray, colors.gray)
|
|
monWrite(math.floor(w * 0.45), 5, "Yield", colors.lightGray, colors.gray)
|
|
monWrite(math.floor(w * 0.60), 5, "Can Make", colors.lightGray, colors.gray)
|
|
monWrite(makeCol, 5, "Go", colors.lightGray, colors.gray)
|
|
|
|
local maxRows = h - 8
|
|
if maxRows < 1 then maxRows = 1 end
|
|
smelterTotalPages = math.max(1, math.ceil(#availList / maxRows))
|
|
if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end
|
|
if smelterPage < 1 then smelterPage = 1 end
|
|
|
|
local startIdx = (smelterPage - 1) * maxRows + 1
|
|
local endIdx = math.min(startIdx + maxRows - 1, #availList)
|
|
|
|
local row = 6
|
|
if #availList == 0 then
|
|
monFill(7, colors.black)
|
|
monCenter(7, "No recipes available to craft", colors.gray, colors.black)
|
|
row = 8
|
|
else
|
|
for i = startIdx, endIdx do
|
|
local r = availList[i]
|
|
local y = row
|
|
local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray
|
|
monFill(y, rowBg)
|
|
|
|
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
|
|
|
|
local maxNameLen = math.floor(w * 0.40)
|
|
local nameDisplay = r.short
|
|
if #nameDisplay > maxNameLen then
|
|
nameDisplay = nameDisplay:sub(1, maxNameLen - 2) .. ".."
|
|
end
|
|
monWrite(4, y, nameDisplay, colors.white, rowBg)
|
|
|
|
monWrite(math.floor(w * 0.45), y, "x" .. r.count, colors.yellow, rowBg)
|
|
monWrite(math.floor(w * 0.60), y,
|
|
string.format("x%d", r.batches), colors.lime, rowBg)
|
|
|
|
if turtleOk then
|
|
monWrite(makeCol, y, " MAKE ", colors.white, colors.green)
|
|
addSmelterZone(makeCol, y, makeCol + 5, y, "craft", r.idx)
|
|
else
|
|
monWrite(makeCol, y, " ---- ", colors.gray, colors.black)
|
|
end
|
|
|
|
row = row + 1
|
|
end
|
|
end
|
|
|
|
while row <= h - 2 do monFill(row, colors.black); row = row + 1 end
|
|
|
|
elseif smelterView == "missing" then
|
|
-- Unavailable Crafting Recipes
|
|
local missList = {}
|
|
for idx, recipe in ipairs(cfg.CRAFTABLE) do
|
|
if not ops.canCraftRecipe(recipe) then
|
|
local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ")
|
|
short = short:sub(1,1):upper() .. short:sub(2)
|
|
local missing = ops.getMissingIngredients(recipe)
|
|
local parts = {}
|
|
for _, m in ipairs(missing) do
|
|
local mShort = m.name:gsub("^minecraft:", ""):gsub("_", " ")
|
|
table.insert(parts, string.format("%s %d/%d", mShort, m.have, m.need))
|
|
end
|
|
table.insert(missList, {
|
|
idx = idx,
|
|
short = short,
|
|
count = recipe.count,
|
|
summary = table.concat(parts, ", "),
|
|
})
|
|
end
|
|
end
|
|
craftAvailCount = #cfg.CRAFTABLE - #missList
|
|
|
|
monFill(5, colors.gray)
|
|
monWrite(2, 5, "#", colors.lightGray, colors.gray)
|
|
monWrite(4, 5, "Output", colors.lightGray, colors.gray)
|
|
monWrite(math.floor(w * 0.35), 5, "Missing (have/need)", colors.lightGray, colors.gray)
|
|
|
|
local maxRows = h - 8
|
|
if maxRows < 1 then maxRows = 1 end
|
|
smelterTotalPages = math.max(1, math.ceil(#missList / maxRows))
|
|
if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end
|
|
if smelterPage < 1 then smelterPage = 1 end
|
|
|
|
local startIdx = (smelterPage - 1) * maxRows + 1
|
|
local endIdx = math.min(startIdx + maxRows - 1, #missList)
|
|
|
|
local row = 6
|
|
if #missList == 0 then
|
|
monFill(7, colors.black)
|
|
monCenter(7, "All recipes can be crafted!", colors.lime, colors.black)
|
|
row = 8
|
|
else
|
|
for i = startIdx, endIdx do
|
|
local r = missList[i]
|
|
local y = row
|
|
local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray
|
|
monFill(y, rowBg)
|
|
|
|
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
|
|
|
|
local nameCol = math.floor(w * 0.35) - 5
|
|
local nameDisplay = r.short .. " x" .. r.count
|
|
if #nameDisplay > nameCol then
|
|
nameDisplay = nameDisplay:sub(1, nameCol - 2) .. ".."
|
|
end
|
|
monWrite(4, y, nameDisplay, colors.white, rowBg)
|
|
|
|
local missCol = math.floor(w * 0.35)
|
|
local missW = w - missCol - 1
|
|
local summaryDisplay = r.summary
|
|
if #summaryDisplay > missW then
|
|
summaryDisplay = summaryDisplay:sub(1, missW - 2) .. ".."
|
|
end
|
|
monWrite(missCol, y, summaryDisplay, colors.red, rowBg)
|
|
|
|
row = row + 1
|
|
end
|
|
end
|
|
|
|
while row <= h - 2 do monFill(row, colors.black); row = row + 1 end
|
|
end
|
|
|
|
-- Pagination (h - 1)
|
|
monFill(h - 1, colors.gray)
|
|
local pageStr = string.format("Pg %d/%d", smelterPage, smelterTotalPages)
|
|
monCenter(h - 1, pageStr, colors.white, colors.gray)
|
|
|
|
if smelterPage > 1 then
|
|
monWrite(2, h - 1, " < ", colors.white, colors.lightGray)
|
|
addSmelterZone(2, h - 1, 4, h - 1, "page_prev", nil)
|
|
end
|
|
if smelterPage < smelterTotalPages then
|
|
monWrite(w - 3, h - 1, " > ", colors.white, colors.lightGray)
|
|
addSmelterZone(w - 3, h - 1, w - 1, h - 1, "page_next", nil)
|
|
end
|
|
|
|
-- Bottom accent
|
|
monFill(h, colors.purple)
|
|
local bottomMsg = ""
|
|
if smelterView == "status" or smelterView == "smelt" then
|
|
local enabledCount = 0
|
|
local totalRecipes = 0
|
|
for _ in pairs(cfg.SMELTABLE) do totalRecipes = totalRecipes + 1 end
|
|
for inputName in pairs(cfg.SMELTABLE) do
|
|
if not state.disabledRecipes[inputName] then enabledCount = enabledCount + 1 end
|
|
end
|
|
bottomMsg = string.format(" Smelt: %d/%d enabled ", enabledCount, totalRecipes)
|
|
if activity.smelting then bottomMsg = " SMELTING... " end
|
|
elseif smelterView == "craft" then
|
|
bottomMsg = " Tap MAKE to craft "
|
|
if activity.crafting then bottomMsg = " CRAFTING... " end
|
|
elseif smelterView == "missing" then
|
|
local availC = craftAvailCount or 0
|
|
bottomMsg = string.format(" Available: %d/%d recipes ", availC, #cfg.CRAFTABLE)
|
|
end
|
|
monCenter(h, bottomMsg, colors.pink, colors.purple)
|
|
|
|
draw.setVisible(true)
|
|
smelterTouchZones = smelterPendingZones
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Touch handlers
|
|
-------------------------------------------------
|
|
|
|
function D.handleTouch(x, y)
|
|
local action, data = hitTest(x, y)
|
|
if not action then
|
|
log.debug("TOUCH", "No zone hit")
|
|
return
|
|
end
|
|
|
|
if action == "amount" then
|
|
selectedAmount = data
|
|
log.debug("UI", "Amount set to %s", data)
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "order" then
|
|
local itemName = data
|
|
if itemName then
|
|
local short = itemName:gsub("^minecraft:", ""):gsub("_", " ")
|
|
state.statusMessage = string.format("Ordering %s x%d...", short, selectedAmount)
|
|
state.statusColor = colors.cyan
|
|
state.statusTimer = 10
|
|
activity.dispensing = true
|
|
state.needsRedraw = true
|
|
ops.orderItem(itemName, selectedAmount)
|
|
end
|
|
|
|
elseif action == "scan" then
|
|
state.statusMessage = "Refreshing..."
|
|
state.statusColor = colors.cyan
|
|
state.statusTimer = 3
|
|
state.needsRedraw = true
|
|
log.debug("UI", "Manual refresh")
|
|
|
|
elseif action == "kb_toggle" then
|
|
showKeyboard = not showKeyboard
|
|
log.debug("UI", "Keyboard %s", showKeyboard and "open" or "closed")
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "kb_key" then
|
|
if #searchQuery < 30 then
|
|
searchQuery = searchQuery .. data
|
|
end
|
|
currentPage = 1
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "kb_bksp" then
|
|
if #searchQuery > 0 then
|
|
searchQuery = searchQuery:sub(1, -2)
|
|
end
|
|
currentPage = 1
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "kb_space" then
|
|
if #searchQuery < 30 then
|
|
searchQuery = searchQuery .. " "
|
|
end
|
|
currentPage = 1
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "kb_done" then
|
|
showKeyboard = false
|
|
log.debug("UI", "Keyboard closed")
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "kb_clear" then
|
|
searchQuery = ""
|
|
currentPage = 1
|
|
log.debug("UI", "Search cleared")
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "page_prev" then
|
|
if currentPage > 1 then
|
|
currentPage = currentPage - 1
|
|
log.debug("UI", "Page %d", currentPage)
|
|
end
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "page_next" then
|
|
if currentPage < totalPages then
|
|
currentPage = currentPage + 1
|
|
log.debug("UI", "Page %d", currentPage)
|
|
end
|
|
state.needsRedraw = true
|
|
end
|
|
end
|
|
|
|
function D.handleSmelterTouch(x, y)
|
|
local action, data = smelterHitTest(x, y)
|
|
if not action then return end
|
|
|
|
if action == "tab" then
|
|
smelterView = data
|
|
smelterPage = 1
|
|
log.debug("UI", "Tab: %s", data)
|
|
state.smelterNeedsRedraw = true
|
|
|
|
elseif action == "toggle_pause" then
|
|
state.smeltingPaused = not state.smeltingPaused
|
|
log.debug("UI", "Smelting %s", state.smeltingPaused and "PAUSED" or "RESUMED")
|
|
ops.saveDisabledRecipes()
|
|
state.smelterNeedsRedraw = true
|
|
state.needsRedraw = true
|
|
|
|
elseif action == "toggle_recipe" then
|
|
if state.disabledRecipes[data] then
|
|
state.disabledRecipes[data] = nil
|
|
else
|
|
state.disabledRecipes[data] = true
|
|
end
|
|
local short = data:gsub("^minecraft:", ""):gsub("_", " ")
|
|
log.debug("UI", "Recipe %s: %s", short, state.disabledRecipes[data] and "OFF" or "ON")
|
|
ops.saveDisabledRecipes()
|
|
state.smelterNeedsRedraw = true
|
|
|
|
elseif action == "enable_all" then
|
|
state.disabledRecipes = {}
|
|
log.debug("UI", "All recipes enabled")
|
|
ops.saveDisabledRecipes()
|
|
state.smelterNeedsRedraw = true
|
|
|
|
elseif action == "disable_all" then
|
|
for inputName in pairs(cfg.SMELTABLE) do
|
|
state.disabledRecipes[inputName] = true
|
|
end
|
|
log.debug("UI", "All recipes disabled")
|
|
ops.saveDisabledRecipes()
|
|
state.smelterNeedsRedraw = true
|
|
|
|
elseif action == "page_prev" then
|
|
if smelterPage > 1 then
|
|
smelterPage = smelterPage - 1
|
|
end
|
|
state.smelterNeedsRedraw = true
|
|
|
|
elseif action == "page_next" then
|
|
if smelterPage < smelterTotalPages then
|
|
smelterPage = smelterPage + 1
|
|
end
|
|
state.smelterNeedsRedraw = true
|
|
|
|
elseif action == "craft" then
|
|
local recipeIdx = data
|
|
local recipe = cfg.CRAFTABLE[recipeIdx]
|
|
if recipe then
|
|
local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ")
|
|
log.info("CRAFT", "Craft request: %s (#%d)", short, recipeIdx)
|
|
local ok, err = ops.craftItem(recipeIdx)
|
|
if ok then
|
|
state.statusMessage = "Crafted " .. short .. " x" .. recipe.count
|
|
state.statusColor = colors.lime
|
|
else
|
|
state.statusMessage = "Craft failed: " .. (err or "unknown")
|
|
state.statusColor = colors.red
|
|
end
|
|
state.statusTimer = 5
|
|
state.needsRedraw = true
|
|
state.smelterNeedsRedraw = true
|
|
end
|
|
end
|
|
end
|
|
|
|
return D
|
|
|
|
end
|