From 5a99543fd61734fffc32ecc4489d437c7f28e580 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 22 Mar 2026 11:33:26 -0400 Subject: [PATCH] Add display module for dashboard rendering and touch handling - 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. --- manager/display.lua | 998 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 998 insertions(+) create mode 100644 manager/display.lua diff --git a/manager/display.lua b/manager/display.lua new file mode 100644 index 0000000..48527e0 --- /dev/null +++ b/manager/display.lua @@ -0,0 +1,998 @@ +-- 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