From 1dca61eeceeeed59c0286144365d51cfe4f3e262 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 22 Mar 2026 01:55:41 -0400 Subject: [PATCH] Add shared UI helpers for inventory management and client interaction --- lib/ui.lua | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 lib/ui.lua diff --git a/lib/ui.lua b/lib/ui.lua new file mode 100644 index 0000000..22294a6 --- /dev/null +++ b/lib/ui.lua @@ -0,0 +1,216 @@ +-- lib/ui.lua — Shared UI helpers for manager & client +-- Usage: local ui = dofile("lib/ui.lua") +-- +-- Set ui.draw to the current render target (window/monitor) +-- before calling any drawing functions. + +local ui = {} + +------------------------------------------------- +-- Drawing target — set this before drawing +------------------------------------------------- + +ui.draw = nil + +------------------------------------------------- +-- Drawing primitives +------------------------------------------------- + +function ui.monWrite(x, y, text, fg, bg) + ui.draw.setCursorPos(x, y) + if fg then ui.draw.setTextColor(fg) end + if bg then ui.draw.setBackgroundColor(bg) end + ui.draw.write(text) +end + +function ui.monFill(y, color) + local w, _ = ui.draw.getSize() + ui.draw.setCursorPos(1, y) + ui.draw.setBackgroundColor(color) + ui.draw.write(string.rep(" ", w)) +end + +function ui.monCenter(y, text, fg, bg) + local w, _ = ui.draw.getSize() + local x = math.floor((w - #text) / 2) + 1 + ui.monWrite(x, y, text, fg, bg) +end + +function ui.monBar(x, y, width, ratio, barColor, bgColor) + local filled = math.floor(ratio * width) + ui.draw.setCursorPos(x, y) + ui.draw.setBackgroundColor(barColor) + ui.draw.write(string.rep(" ", filled)) + ui.draw.setBackgroundColor(bgColor) + ui.draw.write(string.rep(" ", width - filled)) +end + +function ui.drawButton(x, y, text, fg, bg, padLeft, padRight) + padLeft = padLeft or 1 + padRight = padRight or 1 + local full = string.rep(" ", padLeft) .. text .. string.rep(" ", padRight) + ui.monWrite(x, y, full, fg, bg) + return x, y, x + #full - 1, y +end + +------------------------------------------------- +-- Touch zone management +------------------------------------------------- + +--- Append a clickable zone to a pending-zone list. +-- @param zones table — the pending zone array to append to +function ui.addZone(zones, x1, y1, x2, y2, action, data) + table.insert(zones, { + x1 = x1, y1 = y1, x2 = x2, y2 = y2, + action = action, data = data, + }) +end + +--- Find which zone was hit. +-- @param zones table — the active zone array to search +function ui.hitTest(zones, x, y) + for _, zone in ipairs(zones) do + if x >= zone.x1 and x <= zone.x2 and y >= zone.y1 and y <= zone.y2 then + return zone.action, zone.data + end + end + return nil, nil +end + +------------------------------------------------- +-- Monitor setup +------------------------------------------------- + +--- Wrap and configure the main inventory monitor. +-- @param side string — preferred peripheral side/name +-- @param excludeSide string — side to skip (e.g. smelter monitor) +-- @return mon, monName or nil, nil +function ui.setupMonitor(side, excludeSide) + local mon = peripheral.wrap(side) + local monName + if mon and mon.setTextScale then + monName = side + else + mon = nil + end + if not mon then + for _, name in ipairs(peripheral.getNames()) do + if peripheral.getType(name) == "monitor" and name ~= excludeSide then + mon = peripheral.wrap(name) + monName = name + break + end + end + end + if not mon then return nil, nil end + mon.setTextScale(0.5) + mon.clear() + return mon, monName +end + +--- Wrap and configure the smelter monitor. +-- @param side string — preferred peripheral side/name +-- @param excludeName string — main monitor name to skip +-- @return smelterMon, smelterMonName or nil, nil +function ui.setupSmelterMonitor(side, excludeName) + local smelterMon = peripheral.wrap(side) + local smelterMonName + if smelterMon and smelterMon.setTextScale then + smelterMonName = side + else + smelterMon = nil + end + if not smelterMon then + for _, name in ipairs(peripheral.getNames()) do + if peripheral.getType(name) == "monitor" and name ~= excludeName then + smelterMon = peripheral.wrap(name) + smelterMonName = name + break + end + end + end + if not smelterMon then return nil, nil end + smelterMon.setTextScale(0.5) + smelterMon.clear() + return smelterMon, smelterMonName +end + +------------------------------------------------- +-- Item filtering +------------------------------------------------- + +--- Filter an item list by search query. +-- @param itemList table — array of { name, total } +-- @param searchQuery string — filter text (case-insensitive, plain match) +-- @return filtered table — matching subset +function ui.getFilteredItems(itemList, searchQuery) + local filtered = {} + for _, item in ipairs(itemList) do + if searchQuery == "" then + table.insert(filtered, item) + else + local lower = item.name:lower():gsub("minecraft:", ""):gsub("_", " ") + if lower:find(searchQuery:lower(), 1, true) then + table.insert(filtered, item) + end + end + end + return filtered +end + +------------------------------------------------- +-- Crafting recipe helpers +------------------------------------------------- + +--- Count ingredients needed for one craft. +function ui.getRecipeIngredients(recipe) + local ingredients = {} + for _, item in ipairs(recipe.grid) do + if item then + ingredients[item] = (ingredients[item] or 0) + 1 + end + end + return ingredients +end + +--- Check if a recipe can be crafted with current stock. +-- @param recipe table — recipe with .grid +-- @param getTotal function(itemName) -> number — returns stock count +function ui.canCraftRecipe(recipe, getTotal) + local ingredients = ui.getRecipeIngredients(recipe) + for itemName, needed in pairs(ingredients) do + if (getTotal(itemName) or 0) < needed then return false end + end + return true +end + +--- How many batches can be crafted. +-- @param recipe table — recipe with .grid +-- @param getTotal function(itemName) -> number +function ui.maxCraftBatches(recipe, getTotal) + local ingredients = ui.getRecipeIngredients(recipe) + local minBatches = math.huge + for itemName, needed in pairs(ingredients) do + local batches = math.floor((getTotal(itemName) or 0) / needed) + if batches < minBatches then minBatches = batches end + end + if minBatches == math.huge then return 0 end + return minBatches +end + +--- Get list of ingredients where stock < needed. +-- @param recipe table — recipe with .grid +-- @param getTotal function(itemName) -> number +function ui.getMissingIngredients(recipe, getTotal) + local ingredients = ui.getRecipeIngredients(recipe) + local missing = {} + for itemName, needed in pairs(ingredients) do + local have = getTotal(itemName) or 0 + if have < needed then + table.insert(missing, { name = itemName, have = have, need = needed }) + end + end + return missing +end + +return ui