SNiPeRzCiinema

Mob Spawn Controller

Sep 21st, 2026 (edited)
35
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 18.14 KB | None | 0 0
  1. -- mobs.lua
  2. -- Paginated grid of mob buttons. Each button toggles one redstone output on a
  3. -- Redstone Relay; a Create Redstone Link (transmitter) sits against that output.
  4. -- Works on the computer's own screen or on an attached monitor (touch or click).
  5. --
  6. -- Usage:  mobs        run the button screen
  7. --         mobs map    list which relay + side each mob is wired to
  8.  
  9. ------------------------------------------------------------------
  10. -- CONFIG
  11. ------------------------------------------------------------------
  12. -- Just the names, in order. Each mob is assigned the next free relay side
  13. -- automatically, so every relay is wired the same way. Add new mobs at the END
  14. -- so existing wiring doesn't shift.
  15. local MOB_NAMES = {
  16.   "Dreadful Dirt",
  17.   "Wither Skeleton", "Witch", "Enderman", "Slime",
  18.   "Evoker", "Skeleton", "Blaze", "Spider",
  19.   "Magma Cube", "Pig", "Chicken", "Sheep",
  20.   -- add more here
  21. }
  22.  
  23. -- Relay peripheral names, in the order they get filled.
  24. -- Leave empty to auto-detect every Redstone Relay on the network (sorted by name).
  25. local RELAYS = {
  26.   -- "redstone_relay_0",
  27.   -- "redstone_relay_1",
  28. }
  29.  
  30. -- Order sides are used on each relay. "back" is left out because the wired
  31. -- modem sits there; change this if your modem is on a different side.
  32. -- "right" comes before "left" because the relay's own left/right came out
  33. -- mirrored compared to how they're viewed in the world.
  34. local SIDES = { "top", "bottom", "right", "left", "front" }
  35.  
  36. local EXCLUSIVE     = false  -- default for exclusive mode (changeable from the Settings screen)
  37. local TEXT_SCALE    = 0.5    -- default monitor text scale: 0.5 = small (more room), 1 = normal
  38. local STATE_FILE    = "mobs.state"
  39. local SETTINGS_FILE = "mobs.settings"
  40.  
  41. local TITLE      = "Select a Mob"
  42. local BTN_WIDTH  = 14        -- preferred button width
  43. local MIN_WIDTH  = 12        -- narrowest button allowed when squeezing in more columns
  44.  
  45. ------------------------------------------------------------------
  46. -- ASSIGN MOBS TO RELAY SIDES
  47. ------------------------------------------------------------------
  48. local function findRelays()
  49.   if #RELAYS > 0 then return RELAYS end
  50.   local found = {}
  51.   for _, n in ipairs(peripheral.getNames()) do
  52.     if peripheral.getType(n) == "redstone_relay" then found[#found + 1] = n end
  53.   end
  54.   table.sort(found)
  55.   return found
  56. end
  57.  
  58. local relayList = findRelays()
  59. local mobs = {}
  60. for i, name in ipairs(MOB_NAMES) do
  61.   mobs[i] = {
  62.     name  = name,
  63.     relay = relayList[math.ceil(i / #SIDES)],   -- nil if there aren't enough relays
  64.     side  = SIDES[(i - 1) % #SIDES + 1],
  65.   }
  66. end
  67.  
  68. local args = { ... }
  69. if args[1] == "map" then
  70.   local lines = { ("%d relay(s) found, %d mob(s)"):format(#relayList, #mobs) }
  71.   for i, m in ipairs(mobs) do
  72.     lines[#lines + 1] = ("%2d %-15s %s / %s"):format(i, m.name, m.relay or "NO RELAY", m.side)
  73.   end
  74.   textutils.pagedPrint(table.concat(lines, "\n"))
  75.   return
  76. end
  77.  
  78. ------------------------------------------------------------------
  79. -- SETUP
  80. ------------------------------------------------------------------
  81. local mon = peripheral.find("monitor")
  82. local scr = mon or term.current()
  83. local monName = mon and peripheral.getName(mon) or nil
  84.  
  85. local active   = {}   -- active[name] = true/false
  86. local faulty   = {}   -- faulty[name] = true if its relay can't be reached
  87. local buttons  = {}   -- clickable regions
  88. local settings = { exclusive = EXCLUSIVE, textScale = TEXT_SCALE }
  89. local screen   = "main"      -- "main" | "settings" | "confirm"
  90. local pending  = nil         -- action waiting on the confirm screen
  91. local page = 1
  92.  
  93. for _, m in ipairs(mobs) do active[m.name] = false end
  94.  
  95. local function applyScale()
  96.   if mon then pcall(mon.setTextScale, settings.textScale) end
  97. end
  98.  
  99. ------------------------------------------------------------------
  100. -- THEME (custom palette on colour screens)
  101. ------------------------------------------------------------------
  102. -- Advanced monitors can redefine their 16 colours, so the slots below are
  103. -- reused as a nicer palette. The original colours are restored on exit.
  104. local THEME = {
  105.   [colors.black]     = 0x14171C,  -- background
  106.   [colors.gray]      = 0x232830,  -- header / status bar
  107.   [colors.lightGray] = 0x9AA3B2,  -- dim text
  108.   [colors.white]     = 0xF2F4F8,  -- text
  109.   [colors.blue]      = 0x3B4A6B,  -- off: base
  110.   [colors.lightBlue] = 0x566B9A,  -- off: highlight
  111.   [colors.cyan]      = 0x2A3550,  -- off: shadow
  112.   [colors.green]     = 0x2E9E5B,  -- on: base
  113.   [colors.lime]      = 0x4CC582,  -- on: highlight
  114.   [colors.brown]     = 0x1F7042,  -- on: shadow
  115.   [colors.red]       = 0xB03A3A,  -- error: base
  116.   [colors.pink]      = 0xD05A5A,  -- error: highlight
  117.   [colors.magenta]   = 0x7E2A2A,  -- error: shadow
  118.   [colors.orange]    = 0xD9822B,  -- accent
  119.   [colors.yellow]    = 0x9A5A17,  -- accent shadow
  120. }
  121.  
  122. local C = {
  123.   bg = colors.black, panel = colors.gray, dim = colors.lightGray, text = colors.white,
  124.   off    = { base = colors.blue,   light = colors.lightBlue, dark = colors.cyan },
  125.   on     = { base = colors.green,  light = colors.lime,      dark = colors.brown },
  126.   err    = { base = colors.red,    light = colors.pink,      dark = colors.magenta },
  127.   accent = { base = colors.orange, light = colors.orange,    dark = colors.yellow },
  128.   dis    = { base = colors.gray,   light = colors.gray,      dark = colors.gray, fg = colors.lightGray },
  129. }
  130.  
  131. local function applyTheme()
  132.   if not (scr.isColour and scr.isColour()) then return end
  133.   for c, rgb in pairs(THEME) do pcall(scr.setPaletteColour, c, rgb) end
  134. end
  135.  
  136. local function resetTheme()
  137.   if not (scr.isColour and scr.isColour()) then return end
  138.   for c in pairs(THEME) do
  139.     pcall(function() scr.setPaletteColour(c, term.nativePaletteColour(c)) end)
  140.   end
  141. end
  142.  
  143. ------------------------------------------------------------------
  144. -- REDSTONE OUTPUT AND SAVED STATE
  145. ------------------------------------------------------------------
  146. local wrapped = {}
  147. local function getRelay(name)
  148.   local r = wrapped[name]
  149.   if not r then r = peripheral.wrap(name); wrapped[name] = r end
  150.   return r
  151. end
  152.  
  153. local function writeOutput(i)
  154.   local mob = mobs[i]
  155.   local ok = mob.relay and pcall(function()
  156.     local relay = getRelay(mob.relay)
  157.     if not relay then error("missing") end
  158.     relay.setOutput(mob.side, active[mob.name] == true)
  159.   end)
  160.   if mob.relay and not ok then wrapped[mob.relay] = nil end
  161.   faulty[mob.name] = not ok
  162. end
  163.  
  164. local function saveState()
  165.   local f = fs.open(STATE_FILE, "w")
  166.   if f then f.write(textutils.serialize(active)); f.close() end
  167. end
  168.  
  169. local function loadState()
  170.   if not fs.exists(STATE_FILE) then return end
  171.   local f = fs.open(STATE_FILE, "r")
  172.   if not f then return end
  173.   local data = textutils.unserialize(f.readAll())
  174.   f.close()
  175.   if type(data) == "table" then
  176.     for _, m in ipairs(mobs) do active[m.name] = data[m.name] == true end
  177.   end
  178. end
  179.  
  180. local function saveSettings()
  181.   local f = fs.open(SETTINGS_FILE, "w")
  182.   if f then f.write(textutils.serialize(settings)); f.close() end
  183. end
  184.  
  185. local function loadSettings()
  186.   if not fs.exists(SETTINGS_FILE) then return end
  187.   local f = fs.open(SETTINGS_FILE, "r")
  188.   if not f then return end
  189.   local data = textutils.unserialize(f.readAll())
  190.   f.close()
  191.   if type(data) == "table" then
  192.     if type(data.exclusive) == "boolean" then settings.exclusive = data.exclusive end
  193.     if data.textScale == 1 or data.textScale == 0.5 then settings.textScale = data.textScale end
  194.   end
  195. end
  196.  
  197. local function press(i)
  198.   local mob = mobs[i]
  199.   local newState = not active[mob.name]
  200.   if newState and settings.exclusive then
  201.     for j, m in ipairs(mobs) do
  202.       if active[m.name] then active[m.name] = false; writeOutput(j) end
  203.     end
  204.   end
  205.   active[mob.name] = newState
  206.   writeOutput(i)
  207.   saveState()
  208. end
  209.  
  210. local function setAll(state)
  211.   for i, m in ipairs(mobs) do
  212.     active[m.name] = state
  213.     writeOutput(i)
  214.   end
  215.   saveState()
  216. end
  217.  
  218. local function activeCount()
  219.   local n = 0
  220.   for _, m in ipairs(mobs) do
  221.     if active[m.name] then n = n + 1 end
  222.   end
  223.   return n
  224. end
  225.  
  226. -- "Active: Slime, Evoker +2" -- as many names as fit in maxLen characters.
  227. local function activeSummary(maxLen)
  228.   local names = {}
  229.   for _, m in ipairs(mobs) do
  230.     if active[m.name] then names[#names + 1] = m.name end
  231.   end
  232.   if #names == 0 then return "Active: none" end
  233.   for k = #names, 1, -1 do
  234.     local text = "Active: " .. table.concat(names, ", ", 1, k)
  235.     if k < #names then text = text .. " +" .. (#names - k) end
  236.     if #text <= maxLen then return text end
  237.   end
  238.   local text = "Active: " .. names[1]
  239.   if #names > 1 then text = text .. " +" .. (#names - 1) end
  240.   return text
  241. end
  242.  
  243. ------------------------------------------------------------------
  244. -- DRAWING HELPERS
  245. ------------------------------------------------------------------
  246. local function region(x1, y1, x2, y2, action)
  247.   buttons[#buttons + 1] = { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action }
  248. end
  249.  
  250. local function fill(x1, y1, x2, y2, bg)
  251.   scr.setBackgroundColor(bg)
  252.   for y = y1, y2 do
  253.     scr.setCursorPos(x1, y)
  254.     scr.write(string.rep(" ", x2 - x1 + 1))
  255.   end
  256. end
  257.  
  258. local function put(x, y, text, fg, bg)
  259.   local w = scr.getSize()
  260.   if x + #text - 1 > w then text = text:sub(1, w - x + 1) end
  261.   scr.setBackgroundColor(bg)
  262.   scr.setTextColor(fg)
  263.   scr.setCursorPos(x, y)
  264.   scr.write(text)
  265. end
  266.  
  267. local function centered(y, text, x1, x2, fg, bg)
  268.   local w = x2 - x1 + 1
  269.   if #text > w then text = text:sub(1, w) end
  270.   local x = x1 + math.floor((w - #text) / 2)
  271.   put(x, y, text, fg, bg)
  272. end
  273.  
  274. -- A button, 1 to 3 rows tall:
  275. --   3 rows: highlight band / name / shadow band with the state text
  276. --   2 rows: name / shadow band with the state text
  277. --   1 row:  flat, "name state" if it fits
  278. local function drawButton(x1, y1, x2, y2, style, label, sub)
  279.   local fg = style.fg or C.text
  280.   local h, w = y2 - y1 + 1, x2 - x1 + 1
  281.   if h >= 3 then
  282.     fill(x1, y1, x2, y1, style.light)
  283.     fill(x1, y1 + 1, x2, y2 - 1, style.base)
  284.     fill(x1, y2, x2, y2, style.dark)
  285.     centered(y1 + 1, label, x1, x2, fg, style.base)
  286.     if sub then centered(y2, sub, x1, x2, style.fg or C.dim, style.dark) end
  287.   elseif h == 2 then
  288.     fill(x1, y1, x2, y1, style.base)
  289.     fill(x1, y2, x2, y2, style.dark)
  290.     centered(y1, label, x1, x2, fg, style.base)
  291.     if sub then centered(y2, sub, x1, x2, style.fg or C.dim, style.dark) end
  292.   else
  293.     local text = label
  294.     if sub and #label + #sub + 2 <= w then text = label .. " " .. sub end
  295.     fill(x1, y1, x2, y2, style.base)
  296.     centered(y1, text, x1, x2, fg, style.base)
  297.   end
  298. end
  299.  
  300. local function drawHeader(leftText, title, rightLabel, rightAction)
  301.   local w = scr.getSize()
  302.   fill(1, 1, w, 1, C.panel)
  303.   if leftText then put(2, 1, leftText, C.text, C.panel) end
  304.   local rx1 = w - #rightLabel + 1
  305.   if title then
  306.     local tx = math.floor((w - #title) / 2) + 1
  307.     local leftEnd = leftText and (#leftText + 2) or 0
  308.     if tx > leftEnd + 1 and tx + #title - 1 < rx1 - 1 then
  309.       put(tx, 1, title, C.text, C.panel)
  310.     end
  311.   end
  312.   put(rx1, 1, rightLabel, C.text, C.accent.base)
  313.   region(rx1, 1, w, 1, rightAction)
  314. end
  315.  
  316. local function drawStatus(text)
  317.   local w, h = scr.getSize()
  318.   fill(1, h, w, h, C.panel)
  319.   centered(h, text, 1, w, C.dim, C.panel)
  320. end
  321.  
  322. -- Always uses the 3-row raised buttons with a gap between them when there is room
  323. -- (2-row or 1-row only on very short screens). Squeezes in extra columns when
  324. -- that lets everything fit on one page; otherwise the mobs are split into pages.
  325. local function chooseLayout(w, top, bottom)
  326.   local areaH = bottom - top + 1
  327.   local btnH, gapY, rows = 1, 0, math.max(1, areaH)
  328.   for _, s in ipairs({ { 3, 1 }, { 2, 1 }, { 1, 0 } }) do
  329.     local r = math.floor((areaH + s[2]) / (s[1] + s[2]))
  330.     if r >= 1 then btnH, gapY, rows = s[1], s[2], r; break end
  331.   end
  332.   for bw = BTN_WIDTH, MIN_WIDTH, -1 do
  333.     local cols = math.floor((w - 1) / (bw + 1))
  334.     if cols >= 1 and rows * cols >= #mobs then
  335.       return { btnH = btnH, gapY = gapY, rows = rows, cols = cols, cap = rows * cols }
  336.     end
  337.   end
  338.   local cols = math.max(1, math.floor((w - 1) / (BTN_WIDTH + 1)))
  339.   return { btnH = btnH, gapY = gapY, rows = rows, cols = cols, cap = rows * cols }
  340. end
  341.  
  342. ------------------------------------------------------------------
  343. -- SCREENS
  344. ------------------------------------------------------------------
  345. local function drawMain()
  346.   buttons = {}
  347.   local w, h = scr.getSize()
  348.  
  349.   scr.setBackgroundColor(C.bg)
  350.   scr.clear()
  351.  
  352.   local label = (w >= 30) and " Settings " or " Set "
  353.   drawHeader(("On: %d/%d"):format(activeCount(), #mobs), TITLE, label,
  354.     function() screen = "settings" end)
  355.  
  356.   local gapX = 1
  357.   local top = 3
  358.   local L = chooseLayout(w, top, h - 1)          -- everything on one page, no pager row
  359.   if L.cap < #mobs then L = chooseLayout(w, top, h - 2) end   -- otherwise leave room for the pager
  360.   local pages = math.max(1, math.ceil(#mobs / L.cap))
  361.   if page > pages then page = pages end
  362.   if page < 1 then page = 1 end
  363.  
  364.   local btnW = math.floor((w - gapX * (L.cols + 1)) / L.cols)
  365.   local first = (page - 1) * L.cap + 1
  366.   local last = math.min(#mobs, first + L.cap - 1)
  367.   for i = first, last do
  368.     local idx = i - first
  369.     local col = idx % L.cols
  370.     local row = math.floor(idx / L.cols)
  371.     local x1 = gapX + 1 + col * (btnW + gapX)
  372.     local y1 = top + row * (L.btnH + L.gapY)
  373.     local x2, y2 = x1 + btnW - 1, y1 + L.btnH - 1
  374.     local mob = mobs[i]
  375.  
  376.     local style, sub = C.off, "OFF"
  377.     if faulty[mob.name] then style, sub = C.err, "NO LINK"
  378.     elseif active[mob.name] then style, sub = C.on, "ON" end
  379.  
  380.     drawButton(x1, y1, x2, y2, style, mob.name, sub)
  381.     region(x1, y1, x2, y2, function() press(i) end)
  382.   end
  383.  
  384.   if pages > 1 then
  385.     local canPrev, canNext = page > 1, page < pages
  386.     local pBg = canPrev and C.accent.base or C.panel
  387.     local nBg = canNext and C.accent.base or C.panel
  388.     fill(1, h - 1, 8, h - 1, pBg)
  389.     centered(h - 1, "< Prev", 1, 8, canPrev and C.text or C.dim, pBg)
  390.     if canPrev then region(1, h - 1, 8, h - 1, function() page = page - 1 end) end
  391.  
  392.     fill(w - 7, h - 1, w, h - 1, nBg)
  393.     centered(h - 1, "Next >", w - 7, w, canNext and C.text or C.dim, nBg)
  394.     if canNext then region(w - 7, h - 1, w, h - 1, function() page = page + 1 end) end
  395.  
  396.     centered(h - 1, ("Page %d/%d"):format(page, pages), 9, w - 8, C.dim, C.bg)
  397.   end
  398.  
  399.   drawStatus(activeSummary(w - 2))
  400. end
  401.  
  402. local function drawSettings()
  403.   buttons = {}
  404.   local w, h = scr.getSize()
  405.  
  406.   scr.setBackgroundColor(C.bg)
  407.   scr.clear()
  408.   drawHeader(nil, "Settings", " Back ", function() screen = "main" end)
  409.  
  410.   local mx = 2
  411.   local x1, x2 = mx, w - mx + 1
  412.   local btnH = (h >= 16) and 3 or ((h >= 12) and 2 or 1)
  413.   local y = 3
  414.  
  415.   -- exclusive mode toggle
  416.   local ex = settings.exclusive
  417.   drawButton(x1, y, x2, y + btnH - 1, ex and C.on or C.off, "Exclusive mode", ex and "ON" or "OFF")
  418.   region(x1, y, x2, y + btnH - 1, function()
  419.     settings.exclusive = not settings.exclusive
  420.     saveSettings()
  421.   end)
  422.   y = y + btnH
  423.   put(x1, y, "Only one mob can be on at a time", C.dim, C.bg)
  424.   y = y + 2
  425.  
  426.   -- all on / all off
  427.   local bw = math.floor(((x2 - x1 + 1) - 1) / 2)
  428.   local lx1, lx2 = x1, x1 + bw - 1
  429.   local rx1, rx2 = x2 - bw + 1, x2
  430.  
  431.   if ex then
  432.     drawButton(lx1, y, lx2, y + btnH - 1, C.dis, "All ON", "exclusive")
  433.   else
  434.     drawButton(lx1, y, lx2, y + btnH - 1, C.accent, "All ON")
  435.     region(lx1, y, lx2, y + btnH - 1, function()
  436.       pending = { text = "Turn ALL mobs ON?", action = function() setAll(true) end }
  437.       screen = "confirm"
  438.     end)
  439.   end
  440.  
  441.   drawButton(rx1, y, rx2, y + btnH - 1, C.accent, "All OFF")
  442.   region(rx1, y, rx2, y + btnH - 1, function()
  443.     pending = { text = "Turn ALL mobs OFF?", action = function() setAll(false) end }
  444.     screen = "confirm"
  445.   end)
  446.   y = y + btnH + 1
  447.  
  448.   -- text size (monitors only)
  449.   if mon then
  450.     local small = settings.textScale < 1
  451.     drawButton(x1, y, x2, y + btnH - 1, C.off, "Text size", small and "Small" or "Normal")
  452.     region(x1, y, x2, y + btnH - 1, function()
  453.       settings.textScale = small and 1 or 0.5
  454.       saveSettings()
  455.       applyScale()
  456.     end)
  457.   end
  458.  
  459.   drawStatus("Settings are saved automatically")
  460. end
  461.  
  462. local function drawConfirm()
  463.   buttons = {}
  464.   local w, h = scr.getSize()
  465.  
  466.   scr.setBackgroundColor(C.bg)
  467.   scr.clear()
  468.   drawHeader(nil, "Confirm", " Back ", function() screen = "settings" end)
  469.  
  470.   local mx = 2
  471.   local x1, x2 = mx, w - mx + 1
  472.   local btnH = (h >= 16) and 3 or ((h >= 12) and 2 or 1)
  473.   centered(4, pending.text, 1, w, C.text, C.bg)
  474.  
  475.   local y = 6
  476.   local bw = math.floor(((x2 - x1 + 1) - 1) / 2)
  477.   local lx1, lx2 = x1, x1 + bw - 1
  478.   local rx1, rx2 = x2 - bw + 1, x2
  479.  
  480.   drawButton(lx1, y, lx2, y + btnH - 1, C.accent, "Yes")
  481.   region(lx1, y, lx2, y + btnH - 1, function()
  482.     pending.action()
  483.     screen = "main"
  484.   end)
  485.  
  486.   drawButton(rx1, y, rx2, y + btnH - 1, C.off, "Cancel")
  487.   region(rx1, y, rx2, y + btnH - 1, function() screen = "settings" end)
  488.  
  489.   drawStatus("Tap Yes to confirm")
  490. end
  491.  
  492. local function render()
  493.   if screen == "settings" then drawSettings()
  494.   elseif screen == "confirm" then drawConfirm()
  495.   else drawMain() end
  496. end
  497.  
  498. ------------------------------------------------------------------
  499. -- MAIN LOOP
  500. ------------------------------------------------------------------
  501. local function handleClick(x, y)
  502.   for _, b in ipairs(buttons) do
  503.     if x >= b.x1 and x <= b.x2 and y >= b.y1 and y <= b.y2 then
  504.       b.action()
  505.       render()
  506.       return
  507.     end
  508.   end
  509. end
  510.  
  511. local function main()
  512.   loadSettings()
  513.   loadState()
  514.   applyScale()
  515.   for i = 1, #mobs do writeOutput(i) end   -- restore every output to its saved state
  516.   applyTheme()
  517.   render()
  518.   while true do
  519.     local ev, a, b, c = os.pullEvent()
  520.     if ev == "monitor_touch" and a == monName then
  521.       handleClick(b, c)
  522.     elseif ev == "mouse_click" and not mon then
  523.       handleClick(b, c)
  524.     elseif ev == "monitor_resize" or ev == "term_resize" then
  525.       render()
  526.     end
  527.   end
  528. end
  529.  
  530. local ok, err = pcall(main)
  531.  
  532. -- Outputs are left as they were when you exit.
  533. resetTheme()
  534. scr.setBackgroundColor(colors.black)
  535. scr.setTextColor(colors.white)
  536. scr.clear()
  537. scr.setCursorPos(1, 1)
  538. if not ok and err ~= "Terminated" then printError(err) end
Add Comment
Please, Sign In to add comment