Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- mobs.lua
- -- Paginated grid of mob buttons. Each button toggles one redstone output on a
- -- Redstone Relay; a Create Redstone Link (transmitter) sits against that output.
- -- Works on the computer's own screen or on an attached monitor (touch or click).
- --
- -- Usage: mobs run the button screen
- -- mobs map list which relay + side each mob is wired to
- ------------------------------------------------------------------
- -- CONFIG
- ------------------------------------------------------------------
- -- Just the names, in order. Each mob is assigned the next free relay side
- -- automatically, so every relay is wired the same way. Add new mobs at the END
- -- so existing wiring doesn't shift.
- local MOB_NAMES = {
- "Dreadful Dirt",
- "Wither Skeleton", "Witch", "Enderman", "Slime",
- "Evoker", "Skeleton", "Blaze", "Spider",
- "Magma Cube", "Pig", "Chicken", "Sheep",
- -- add more here
- }
- -- Relay peripheral names, in the order they get filled.
- -- Leave empty to auto-detect every Redstone Relay on the network (sorted by name).
- local RELAYS = {
- -- "redstone_relay_0",
- -- "redstone_relay_1",
- }
- -- Order sides are used on each relay. "back" is left out because the wired
- -- modem sits there; change this if your modem is on a different side.
- -- "right" comes before "left" because the relay's own left/right came out
- -- mirrored compared to how they're viewed in the world.
- local SIDES = { "top", "bottom", "right", "left", "front" }
- local EXCLUSIVE = false -- default for exclusive mode (changeable from the Settings screen)
- local TEXT_SCALE = 0.5 -- default monitor text scale: 0.5 = small (more room), 1 = normal
- local STATE_FILE = "mobs.state"
- local SETTINGS_FILE = "mobs.settings"
- local TITLE = "Select a Mob"
- local BTN_WIDTH = 14 -- preferred button width
- local MIN_WIDTH = 12 -- narrowest button allowed when squeezing in more columns
- ------------------------------------------------------------------
- -- ASSIGN MOBS TO RELAY SIDES
- ------------------------------------------------------------------
- local function findRelays()
- if #RELAYS > 0 then return RELAYS end
- local found = {}
- for _, n in ipairs(peripheral.getNames()) do
- if peripheral.getType(n) == "redstone_relay" then found[#found + 1] = n end
- end
- table.sort(found)
- return found
- end
- local relayList = findRelays()
- local mobs = {}
- for i, name in ipairs(MOB_NAMES) do
- mobs[i] = {
- name = name,
- relay = relayList[math.ceil(i / #SIDES)], -- nil if there aren't enough relays
- side = SIDES[(i - 1) % #SIDES + 1],
- }
- end
- local args = { ... }
- if args[1] == "map" then
- local lines = { ("%d relay(s) found, %d mob(s)"):format(#relayList, #mobs) }
- for i, m in ipairs(mobs) do
- lines[#lines + 1] = ("%2d %-15s %s / %s"):format(i, m.name, m.relay or "NO RELAY", m.side)
- end
- textutils.pagedPrint(table.concat(lines, "\n"))
- return
- end
- ------------------------------------------------------------------
- -- SETUP
- ------------------------------------------------------------------
- local mon = peripheral.find("monitor")
- local scr = mon or term.current()
- local monName = mon and peripheral.getName(mon) or nil
- local active = {} -- active[name] = true/false
- local faulty = {} -- faulty[name] = true if its relay can't be reached
- local buttons = {} -- clickable regions
- local settings = { exclusive = EXCLUSIVE, textScale = TEXT_SCALE }
- local screen = "main" -- "main" | "settings" | "confirm"
- local pending = nil -- action waiting on the confirm screen
- local page = 1
- for _, m in ipairs(mobs) do active[m.name] = false end
- local function applyScale()
- if mon then pcall(mon.setTextScale, settings.textScale) end
- end
- ------------------------------------------------------------------
- -- THEME (custom palette on colour screens)
- ------------------------------------------------------------------
- -- Advanced monitors can redefine their 16 colours, so the slots below are
- -- reused as a nicer palette. The original colours are restored on exit.
- local THEME = {
- [colors.black] = 0x14171C, -- background
- [colors.gray] = 0x232830, -- header / status bar
- [colors.lightGray] = 0x9AA3B2, -- dim text
- [colors.white] = 0xF2F4F8, -- text
- [colors.blue] = 0x3B4A6B, -- off: base
- [colors.lightBlue] = 0x566B9A, -- off: highlight
- [colors.cyan] = 0x2A3550, -- off: shadow
- [colors.green] = 0x2E9E5B, -- on: base
- [colors.lime] = 0x4CC582, -- on: highlight
- [colors.brown] = 0x1F7042, -- on: shadow
- [colors.red] = 0xB03A3A, -- error: base
- [colors.pink] = 0xD05A5A, -- error: highlight
- [colors.magenta] = 0x7E2A2A, -- error: shadow
- [colors.orange] = 0xD9822B, -- accent
- [colors.yellow] = 0x9A5A17, -- accent shadow
- }
- local C = {
- bg = colors.black, panel = colors.gray, dim = colors.lightGray, text = colors.white,
- off = { base = colors.blue, light = colors.lightBlue, dark = colors.cyan },
- on = { base = colors.green, light = colors.lime, dark = colors.brown },
- err = { base = colors.red, light = colors.pink, dark = colors.magenta },
- accent = { base = colors.orange, light = colors.orange, dark = colors.yellow },
- dis = { base = colors.gray, light = colors.gray, dark = colors.gray, fg = colors.lightGray },
- }
- local function applyTheme()
- if not (scr.isColour and scr.isColour()) then return end
- for c, rgb in pairs(THEME) do pcall(scr.setPaletteColour, c, rgb) end
- end
- local function resetTheme()
- if not (scr.isColour and scr.isColour()) then return end
- for c in pairs(THEME) do
- pcall(function() scr.setPaletteColour(c, term.nativePaletteColour(c)) end)
- end
- end
- ------------------------------------------------------------------
- -- REDSTONE OUTPUT AND SAVED STATE
- ------------------------------------------------------------------
- local wrapped = {}
- local function getRelay(name)
- local r = wrapped[name]
- if not r then r = peripheral.wrap(name); wrapped[name] = r end
- return r
- end
- local function writeOutput(i)
- local mob = mobs[i]
- local ok = mob.relay and pcall(function()
- local relay = getRelay(mob.relay)
- if not relay then error("missing") end
- relay.setOutput(mob.side, active[mob.name] == true)
- end)
- if mob.relay and not ok then wrapped[mob.relay] = nil end
- faulty[mob.name] = not ok
- end
- local function saveState()
- local f = fs.open(STATE_FILE, "w")
- if f then f.write(textutils.serialize(active)); f.close() end
- end
- local function loadState()
- if not fs.exists(STATE_FILE) then return end
- local f = fs.open(STATE_FILE, "r")
- if not f then return end
- local data = textutils.unserialize(f.readAll())
- f.close()
- if type(data) == "table" then
- for _, m in ipairs(mobs) do active[m.name] = data[m.name] == true end
- end
- end
- local function saveSettings()
- local f = fs.open(SETTINGS_FILE, "w")
- if f then f.write(textutils.serialize(settings)); f.close() end
- end
- local function loadSettings()
- if not fs.exists(SETTINGS_FILE) then return end
- local f = fs.open(SETTINGS_FILE, "r")
- if not f then return end
- local data = textutils.unserialize(f.readAll())
- f.close()
- if type(data) == "table" then
- if type(data.exclusive) == "boolean" then settings.exclusive = data.exclusive end
- if data.textScale == 1 or data.textScale == 0.5 then settings.textScale = data.textScale end
- end
- end
- local function press(i)
- local mob = mobs[i]
- local newState = not active[mob.name]
- if newState and settings.exclusive then
- for j, m in ipairs(mobs) do
- if active[m.name] then active[m.name] = false; writeOutput(j) end
- end
- end
- active[mob.name] = newState
- writeOutput(i)
- saveState()
- end
- local function setAll(state)
- for i, m in ipairs(mobs) do
- active[m.name] = state
- writeOutput(i)
- end
- saveState()
- end
- local function activeCount()
- local n = 0
- for _, m in ipairs(mobs) do
- if active[m.name] then n = n + 1 end
- end
- return n
- end
- -- "Active: Slime, Evoker +2" -- as many names as fit in maxLen characters.
- local function activeSummary(maxLen)
- local names = {}
- for _, m in ipairs(mobs) do
- if active[m.name] then names[#names + 1] = m.name end
- end
- if #names == 0 then return "Active: none" end
- for k = #names, 1, -1 do
- local text = "Active: " .. table.concat(names, ", ", 1, k)
- if k < #names then text = text .. " +" .. (#names - k) end
- if #text <= maxLen then return text end
- end
- local text = "Active: " .. names[1]
- if #names > 1 then text = text .. " +" .. (#names - 1) end
- return text
- end
- ------------------------------------------------------------------
- -- DRAWING HELPERS
- ------------------------------------------------------------------
- local function region(x1, y1, x2, y2, action)
- buttons[#buttons + 1] = { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action }
- end
- local function fill(x1, y1, x2, y2, bg)
- scr.setBackgroundColor(bg)
- for y = y1, y2 do
- scr.setCursorPos(x1, y)
- scr.write(string.rep(" ", x2 - x1 + 1))
- end
- end
- local function put(x, y, text, fg, bg)
- local w = scr.getSize()
- if x + #text - 1 > w then text = text:sub(1, w - x + 1) end
- scr.setBackgroundColor(bg)
- scr.setTextColor(fg)
- scr.setCursorPos(x, y)
- scr.write(text)
- end
- local function centered(y, text, x1, x2, fg, bg)
- local w = x2 - x1 + 1
- if #text > w then text = text:sub(1, w) end
- local x = x1 + math.floor((w - #text) / 2)
- put(x, y, text, fg, bg)
- end
- -- A button, 1 to 3 rows tall:
- -- 3 rows: highlight band / name / shadow band with the state text
- -- 2 rows: name / shadow band with the state text
- -- 1 row: flat, "name state" if it fits
- local function drawButton(x1, y1, x2, y2, style, label, sub)
- local fg = style.fg or C.text
- local h, w = y2 - y1 + 1, x2 - x1 + 1
- if h >= 3 then
- fill(x1, y1, x2, y1, style.light)
- fill(x1, y1 + 1, x2, y2 - 1, style.base)
- fill(x1, y2, x2, y2, style.dark)
- centered(y1 + 1, label, x1, x2, fg, style.base)
- if sub then centered(y2, sub, x1, x2, style.fg or C.dim, style.dark) end
- elseif h == 2 then
- fill(x1, y1, x2, y1, style.base)
- fill(x1, y2, x2, y2, style.dark)
- centered(y1, label, x1, x2, fg, style.base)
- if sub then centered(y2, sub, x1, x2, style.fg or C.dim, style.dark) end
- else
- local text = label
- if sub and #label + #sub + 2 <= w then text = label .. " " .. sub end
- fill(x1, y1, x2, y2, style.base)
- centered(y1, text, x1, x2, fg, style.base)
- end
- end
- local function drawHeader(leftText, title, rightLabel, rightAction)
- local w = scr.getSize()
- fill(1, 1, w, 1, C.panel)
- if leftText then put(2, 1, leftText, C.text, C.panel) end
- local rx1 = w - #rightLabel + 1
- if title then
- local tx = math.floor((w - #title) / 2) + 1
- local leftEnd = leftText and (#leftText + 2) or 0
- if tx > leftEnd + 1 and tx + #title - 1 < rx1 - 1 then
- put(tx, 1, title, C.text, C.panel)
- end
- end
- put(rx1, 1, rightLabel, C.text, C.accent.base)
- region(rx1, 1, w, 1, rightAction)
- end
- local function drawStatus(text)
- local w, h = scr.getSize()
- fill(1, h, w, h, C.panel)
- centered(h, text, 1, w, C.dim, C.panel)
- end
- -- Always uses the 3-row raised buttons with a gap between them when there is room
- -- (2-row or 1-row only on very short screens). Squeezes in extra columns when
- -- that lets everything fit on one page; otherwise the mobs are split into pages.
- local function chooseLayout(w, top, bottom)
- local areaH = bottom - top + 1
- local btnH, gapY, rows = 1, 0, math.max(1, areaH)
- for _, s in ipairs({ { 3, 1 }, { 2, 1 }, { 1, 0 } }) do
- local r = math.floor((areaH + s[2]) / (s[1] + s[2]))
- if r >= 1 then btnH, gapY, rows = s[1], s[2], r; break end
- end
- for bw = BTN_WIDTH, MIN_WIDTH, -1 do
- local cols = math.floor((w - 1) / (bw + 1))
- if cols >= 1 and rows * cols >= #mobs then
- return { btnH = btnH, gapY = gapY, rows = rows, cols = cols, cap = rows * cols }
- end
- end
- local cols = math.max(1, math.floor((w - 1) / (BTN_WIDTH + 1)))
- return { btnH = btnH, gapY = gapY, rows = rows, cols = cols, cap = rows * cols }
- end
- ------------------------------------------------------------------
- -- SCREENS
- ------------------------------------------------------------------
- local function drawMain()
- buttons = {}
- local w, h = scr.getSize()
- scr.setBackgroundColor(C.bg)
- scr.clear()
- local label = (w >= 30) and " Settings " or " Set "
- drawHeader(("On: %d/%d"):format(activeCount(), #mobs), TITLE, label,
- function() screen = "settings" end)
- local gapX = 1
- local top = 3
- local L = chooseLayout(w, top, h - 1) -- everything on one page, no pager row
- if L.cap < #mobs then L = chooseLayout(w, top, h - 2) end -- otherwise leave room for the pager
- local pages = math.max(1, math.ceil(#mobs / L.cap))
- if page > pages then page = pages end
- if page < 1 then page = 1 end
- local btnW = math.floor((w - gapX * (L.cols + 1)) / L.cols)
- local first = (page - 1) * L.cap + 1
- local last = math.min(#mobs, first + L.cap - 1)
- for i = first, last do
- local idx = i - first
- local col = idx % L.cols
- local row = math.floor(idx / L.cols)
- local x1 = gapX + 1 + col * (btnW + gapX)
- local y1 = top + row * (L.btnH + L.gapY)
- local x2, y2 = x1 + btnW - 1, y1 + L.btnH - 1
- local mob = mobs[i]
- local style, sub = C.off, "OFF"
- if faulty[mob.name] then style, sub = C.err, "NO LINK"
- elseif active[mob.name] then style, sub = C.on, "ON" end
- drawButton(x1, y1, x2, y2, style, mob.name, sub)
- region(x1, y1, x2, y2, function() press(i) end)
- end
- if pages > 1 then
- local canPrev, canNext = page > 1, page < pages
- local pBg = canPrev and C.accent.base or C.panel
- local nBg = canNext and C.accent.base or C.panel
- fill(1, h - 1, 8, h - 1, pBg)
- centered(h - 1, "< Prev", 1, 8, canPrev and C.text or C.dim, pBg)
- if canPrev then region(1, h - 1, 8, h - 1, function() page = page - 1 end) end
- fill(w - 7, h - 1, w, h - 1, nBg)
- centered(h - 1, "Next >", w - 7, w, canNext and C.text or C.dim, nBg)
- if canNext then region(w - 7, h - 1, w, h - 1, function() page = page + 1 end) end
- centered(h - 1, ("Page %d/%d"):format(page, pages), 9, w - 8, C.dim, C.bg)
- end
- drawStatus(activeSummary(w - 2))
- end
- local function drawSettings()
- buttons = {}
- local w, h = scr.getSize()
- scr.setBackgroundColor(C.bg)
- scr.clear()
- drawHeader(nil, "Settings", " Back ", function() screen = "main" end)
- local mx = 2
- local x1, x2 = mx, w - mx + 1
- local btnH = (h >= 16) and 3 or ((h >= 12) and 2 or 1)
- local y = 3
- -- exclusive mode toggle
- local ex = settings.exclusive
- drawButton(x1, y, x2, y + btnH - 1, ex and C.on or C.off, "Exclusive mode", ex and "ON" or "OFF")
- region(x1, y, x2, y + btnH - 1, function()
- settings.exclusive = not settings.exclusive
- saveSettings()
- end)
- y = y + btnH
- put(x1, y, "Only one mob can be on at a time", C.dim, C.bg)
- y = y + 2
- -- all on / all off
- local bw = math.floor(((x2 - x1 + 1) - 1) / 2)
- local lx1, lx2 = x1, x1 + bw - 1
- local rx1, rx2 = x2 - bw + 1, x2
- if ex then
- drawButton(lx1, y, lx2, y + btnH - 1, C.dis, "All ON", "exclusive")
- else
- drawButton(lx1, y, lx2, y + btnH - 1, C.accent, "All ON")
- region(lx1, y, lx2, y + btnH - 1, function()
- pending = { text = "Turn ALL mobs ON?", action = function() setAll(true) end }
- screen = "confirm"
- end)
- end
- drawButton(rx1, y, rx2, y + btnH - 1, C.accent, "All OFF")
- region(rx1, y, rx2, y + btnH - 1, function()
- pending = { text = "Turn ALL mobs OFF?", action = function() setAll(false) end }
- screen = "confirm"
- end)
- y = y + btnH + 1
- -- text size (monitors only)
- if mon then
- local small = settings.textScale < 1
- drawButton(x1, y, x2, y + btnH - 1, C.off, "Text size", small and "Small" or "Normal")
- region(x1, y, x2, y + btnH - 1, function()
- settings.textScale = small and 1 or 0.5
- saveSettings()
- applyScale()
- end)
- end
- drawStatus("Settings are saved automatically")
- end
- local function drawConfirm()
- buttons = {}
- local w, h = scr.getSize()
- scr.setBackgroundColor(C.bg)
- scr.clear()
- drawHeader(nil, "Confirm", " Back ", function() screen = "settings" end)
- local mx = 2
- local x1, x2 = mx, w - mx + 1
- local btnH = (h >= 16) and 3 or ((h >= 12) and 2 or 1)
- centered(4, pending.text, 1, w, C.text, C.bg)
- local y = 6
- local bw = math.floor(((x2 - x1 + 1) - 1) / 2)
- local lx1, lx2 = x1, x1 + bw - 1
- local rx1, rx2 = x2 - bw + 1, x2
- drawButton(lx1, y, lx2, y + btnH - 1, C.accent, "Yes")
- region(lx1, y, lx2, y + btnH - 1, function()
- pending.action()
- screen = "main"
- end)
- drawButton(rx1, y, rx2, y + btnH - 1, C.off, "Cancel")
- region(rx1, y, rx2, y + btnH - 1, function() screen = "settings" end)
- drawStatus("Tap Yes to confirm")
- end
- local function render()
- if screen == "settings" then drawSettings()
- elseif screen == "confirm" then drawConfirm()
- else drawMain() end
- end
- ------------------------------------------------------------------
- -- MAIN LOOP
- ------------------------------------------------------------------
- local function handleClick(x, y)
- for _, b in ipairs(buttons) do
- if x >= b.x1 and x <= b.x2 and y >= b.y1 and y <= b.y2 then
- b.action()
- render()
- return
- end
- end
- end
- local function main()
- loadSettings()
- loadState()
- applyScale()
- for i = 1, #mobs do writeOutput(i) end -- restore every output to its saved state
- applyTheme()
- render()
- while true do
- local ev, a, b, c = os.pullEvent()
- if ev == "monitor_touch" and a == monName then
- handleClick(b, c)
- elseif ev == "mouse_click" and not mon then
- handleClick(b, c)
- elseif ev == "monitor_resize" or ev == "term_resize" then
- render()
- end
- end
- end
- local ok, err = pcall(main)
- -- Outputs are left as they were when you exit.
- resetTheme()
- scr.setBackgroundColor(colors.black)
- scr.setTextColor(colors.white)
- scr.clear()
- scr.setCursorPos(1, 1)
- if not ok and err ~= "Terminated" then printError(err) end
Add Comment
Please, Sign In to add comment