Files
Inventory-Manager-CC/manager/display.lua.bak
MayaTheShy d4a3b1cce9 Add dashboard rendering and touch handlers for inventory management
- Implemented main dashboard UI with item display, status bar, and pagination.
- Added touch zones for item ordering, quantity selection, and search functionality.
- Created smelter dashboard with tabs for status, smelting, crafting, and missing recipes.
- Integrated keyboard input for search queries and item management.
- Enhanced drawing functions for better UI representation and interaction.
2026-03-22 16:56:50 -04:00

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