local gui = require("gui") local color = require("gui.core.color") local loader = require("loader") local theme = require("gui.core.theme") local fmt = require("fmt") local timer = require("utils") local multi, thread = require("multi"):init() local menu = require("gui.core.menus") local timesup = love.audio.newSource("assets/sounds/timesup.mp3", "static") local double = love.audio.newSource("assets/sounds/double.mp3", "static") local boardUpdater = gui:getProcessor():newProcessor("board-updater") local scoreUpdater = gui:getProcessor():newProcessor("score-updater") local qUpdater = gui:getProcessor():newProcessor("question-updater") local activePlayer local playerList = {} local playerStaticList = {} local scoreboard = {} -- Create a table to manage GUI elements local manage = {} -- Function to resize fonts for all managed elements local function resizeFonts() -- Use multi-threading to improve performance multi:newThread(function() -- Introduce a small delay to avoid too many iterations at once thread.skip(2) -- Iterate over each element in the 'manage' table for i = 1, #manage do local elem = manage[i] -- Check if the current element has a centerFont property if elem.centerFont then -- Adjust font size and re-center the text elem:fitFont(nil, nil, {scale = 2/3}) elem:centerFont() end end end) end gui.Events.OnResized(resizeFonts) local completed_questions = 0 local min_questions = 0 local dd_count = 0 local dd_enabled = false local applied_dd = false local board, question local function pickUniqueIndices(t, count) assert(#t >= count, "Not enough elements to pick " .. count .. " unique items") local indices = {} for i = 1, #t do indices[i] = i end -- partial Fisher-Yates shuffle for i = 1, count do local j = math.random(i, #indices) indices[i], indices[j] = indices[j], indices[i] end return unpack(indices, 1, count) end local function pickFiltered(items, count) -- Filter to only elements whose text contains "$" local filtered = {} for _, item in ipairs(items) do if item.text:find("%$") then filtered[#filtered + 1] = item end end -- Use pickUniqueIndices to select from filtered results local picks = { pickUniqueIndices(filtered, count) } local results = {} for _, idx in ipairs(picks) do results[#results + 1] = filtered[idx] end return unpack(results) end function applyDD() if not applied_dd and completed_questions > min_questions then local dd = 0 local dd_list = {} local dds = {pickFiltered(board.children, dd_count)} for i,v in pairs(dds) do v.isDouble = true end applied_dd = true end end function LoadTemplate(name, path) if love.filesystem.getInfo(path .."/templates/" .. name .. ".lua") then data = love.filesystem.read(path .."/templates/" .. name .. ".lua") elseif love.filesystem.getInfo("templates/" .. name .. ".lua") then data = love.filesystem.read("templates/" .. name .. ".lua") else print("Error Loading Template: " .. name, path) gui:newMessageBox({ title = "Error!", message = "Unable to load template: " .. name, buttons = { "Yes", "No" }, onChoice = function(label, idx) if label == "Yes" then doCallback(selected) pickerWindow.visible = false pickerWindow:setTag("visual") end end, }) return end local template, err = loadstring(data) if err ~= nil then error(err) end local timer = function(wait, callback) multi:newAlarm(wait):OnRing(callback) end local env = { -- lua built-ins except io/os code execution pairs=pairs, print=print, tostring=tostring, tonumber=tonumber, type=type, assert=assert, ipairs=ipairs, table=table, math=math, string=string, next=next, pcall=pcall, select=select, xpcall=xpcall, utf8=utf8, clock = require("socket").gettime, ALIGN_CENTER = gui.ALIGN_CENTER, ALIGN_LEFT = gui.ALIGN_LEFT, ALIGN_RIGHT = gui.ALIGN_RIGHT, ALIGN_JUSTIFY = gui.ALIGN_JUSTIFY, -- Global vars color = color, error = multi.error, theme = theme, gui = gui, timer = timer, -- love stuff newSource = love.audio.newSource } env._G = env local isoTemplate = multi.isolateFunction(template, env) return isoTemplate() end local function manageCosmetics(frame, cosmetics) if cosmetics["bg-img"] then frame:setImage(cosmetics["bg-img"]) else frame:setImage("assets/images/background.png") end if type(cosmetics.colors) == "table" then for i,v in pairs(cosmetics.colors) do color.reindexColor((i:gsub("-","_")),v) end end end function gui:cleanup() for i = #self.children, 1, -1 do self.children[i]:destroy() end self.children = {} completed_questions = completed_questions + 1 end local reset = boardUpdater:newConnection() local default_text = "Pick a Category" function Menu(frame, path) if path:match(".zip") then -- local basename = path:match("([^/\\]+)$") print(path) local success = love.filesystem.mount(path, "quiz", true) if not success then gui:newMessageBox({ title = "Return to the main menu?", message = "Couldn't load file: " .. path, buttons = { "Return", "Quit" }, onChoice = function(label, idx) if label == "Return" then return menu.getMenu("title"):visible(true) else os.exit() end end, }) return end path = "quiz" end -- Start the processors now boardUpdater.Start() qUpdater.Start() scoreUpdater.Start() local scoreboard = ScoreBoard(frame, 0, 0, 0, 0, .06, .1, .170, .8) local qframe = frame:newFrame(0, 0, 0, 0, .24, .1, .70, .8) local label = frame:newTextLabel(default_text, 0, 10, 0, -20, .29, 0, .6, .1) label.visibility = .85 label.drawBorder = false label.align = gui.ALIGN_CENTER label.color = color.bg label.textColor = color.status_font label:setRoundness(20,20,60) boardUpdater:newThread(function() local test = function() return label.text ~= "" end while true do thread.hold(test) label:centerFont() label:fitFont(nil, nil, {scale = 2/3}) end end) local data = loader:new(path) board, question, dailydouble = qframe:newFrame(), qframe:newFrame(), qframe:newImageLabel("assets/images/double.jpg") index = data.index local cosmetics = index.cosmetics or {} manageCosmetics(frame, cosmetics) board:fullFrame() question:fullFrame() dailydouble:fullFrame() dailydouble.visible = false question.visible = false question.color = color.question_bg local tiers = index.settings.tiers or 5 local start = index.settings.start or 100 local inc = index.settings.increment or 100 if index.settings.dailyDouble and index.settings.dailyDouble.enabled then local dd = index.settings.dailyDouble dd_enabled = dd.enabled min_questions = dd.minQuestions dd_count = dd.count end local autoreveal = false if index.dev and index.dev.enabled then if index.dev["auto-reveal"] then autoreveal = true end if type(index.dev.players) == "table" then for i,v in ipairs(index.dev.players) do if type(v) == "string" then scoreboard:AddPlayer(v, 0) elseif type(v) == "table" then scoreboard:AddPlayer(v.name, v.score, v.icon) end end end end reset(function() question:cleanup() manageCosmetics(frame, cosmetics, true) label.text = default_text end) for cat,v in pairs(index.categories) do local c if v.image then c = board:newImageLabel(v.image, 0, 0, 0, 0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1)) else c = board:newTextLabel(v.displayName or v.name,0,0,0,0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1)) c.align = gui.ALIGN_CENTER c.textColor = color.header_font c.color = color.header_tile end c.UUID = multi.generate_uuid7() -- Each otpion gets a unique UUID if not autoreveal then img = c:newImageButton("assets/images/placeholder.jpg") img.visibility = 0 img:OnReleased(function(self) self.visible = false end) img:fullFrame() end table.insert(manage,c) for tier = 1,tiers do local t = board:newTextButton("$" .. start + inc*(tier-1),0,0,0,0,(1/#index.categories)*(cat-1),1/(tiers+1)*tier,(1/#index.categories),1/(tiers+1)) t.textColor = color.tile_font t.align = gui.ALIGN_CENTER t.color = color.tile_bg t.category = v.name t.index = tier t.price = start + inc*(tier-1) t:OnReleased(boardUpdater:newFunction(function(self) if self.text == "" or GetActivePlayer() == nil then return end if dd_enabled then applyDD() -- check and run DD if conditions meet end if index.categories[cat].questions == nil then fmt.Printf("Question not defined: File: %v Category: %v - %v\n",path,index.categories[cat].name,start + inc*(tier-1)) return end local globals = index.categories[cat].global if not globals then globals = {} end local q = index.categories[cat].questions[self.index] if not q then return end label.text = t.category .. ": " .. t.price local mt = debug.getmetatable(q) if mt then mt.__metatable = nil end setmetatable(q, {__index = globals}) if q == nil then fmt.Printf("Question contains no data: File: %v Category: %v Tier: %v\n",path,index.categories[cat].name,start + inc*(tier-1)) return end if q.cosmetics then manageCosmetics(frame, q.cosmetics) end self.textVisibility = 0 local template = LoadTemplate(q.template, path) question.visible = true local player = GetActivePlayer() local tm local stop fmt.Printf("--------------------\nQuestion: %v \nAnswer: %v\n--------------------\n",q["title"],q["answer"]) local mul = 1 boardUpdater:newThread(function() if self.isDouble then mul = 2 double:play() dailydouble.visible = true thread.hold(function() return not double:isPlaying() end) dailydouble.visible = false end if q["time-limit"] then tm = timer.startTimer({duration = q["time-limit"]}) tm.OnStop(function() -- Make sound? Subtract if daily double if stop then return end timesup:play() end) end local finished = false template.index(question, q, function(ans, m) local m = m or 1 if finished then return end player = GetActivePlayer() if tm then tm:Cleanup() end stop = true if ans then player:Add((self.price*mul)*m) finished = true question.visible = false reset:Fire() elseif ans == false then player:Add(-self.price*mul) player = GetNextPlayer() else finished = true question.visible = false reset:Fire() end -- nil is a valid option where you weren't right or wrong, you just skipped self.text = "" end) boardUpdater:newThread("QuestionUpdater",function() while true do template.update(dt) thread.yield() if self.text == "" then return end end end) end) end)) table.insert(manage,t) end end resizeFonts() resizeFonts() return background end function GetActivePlayer() if not activePlayer then return end return activePlayer.link end local function GetPlayerPos() for i,v in pairs(playerStaticList) do if v == GetActivePlayer() then return i end end end function GetNextPlayer() local pos = GetPlayerPos() if pos >= #playerStaticList then activePlayer = playerStaticList[1].Ref.Frame else activePlayer = playerStaticList[pos + 1].Ref.Frame end scoreboard:RenderPlayer(playerList) return GetActivePlayer() end function love.filedropped(file) file:open("r") local data = file:read() print("Load file? " .. file:getFilename()) end function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh) -- Colors local C_BG_PANEL = color.new("#1a1a2e") local C_BG_HEADER = color.new("#16213e") local C_ACCENT = color.new("#e94560") local C_ROW_TOP = color.new("#1c2641") local C_ROW_NORM = color.new("#121226") local C_BORDER_TOP = color.new("#323c6e") local C_BORDER_NRM = color.new("#1c1c37") local C_BAR_EMPTY = color.new("#232341") local C_TEXT_MUTED = color.new("#786e96") local C_WHITE = color.new("#ffffff") local C_GOLD = color.new("#ffd700") local C_SILVER = color.new("#C0C0C0") local C_BRONZE = color.new("#cd7f32") -- Config stuff local LEADER_HEIGHT_SCALE = .06 local HEIGHT_SCALE = .03 local PLAYER_HEIGHT = .05 -- local rankColors = { C_GOLD, C_SILVER, C_BRONZE } local leaderboard = frame:newFrame(x, y, w, h, sx, sy, sw, sh) leaderboard.color = C_BORDER_NRM local headernum = leaderboard:newTextLabel("#",0,0,0,0,0,LEADER_HEIGHT_SCALE,1/6,HEIGHT_SCALE) headernum.align = gui.ALIGN_CENTER local headerplayer = leaderboard:newTextLabel("PLAYER",0,0,0,0,1/6,LEADER_HEIGHT_SCALE,3/6,HEIGHT_SCALE) headerplayer.align = gui.ALIGN_LEFT local headerscore = leaderboard:newTextLabel("SCORE",0,0,0,0,4/6,LEADER_HEIGHT_SCALE,2/6,HEIGHT_SCALE) headerscore.align = gui.ALIGN_RIGHT local header = leaderboard:newTextLabel("LEADERBOARD",0,0,0,0,0,0,1,LEADER_HEIGHT_SCALE) header.borderColor = C_ACCENT header.textColor = C_ACCENT header.color = C_BG_HEADER header.align = gui.ALIGN_CENTER local BASE_HEIGHT = LEADER_HEIGHT_SCALE + HEIGHT_SCALE gui.apply({ drawBorder = false, color = color.new("#0f2a60"), textColor = C_TEXT_MUTED, }, headernum, headerplayer, headerscore) local updateList = {header, headernum, headerplayer, headerscore} local function ScoreResize() scoreUpdater:newThread(function() thread.skip(2) for _,object in pairs(updateList) do object:fitFont(nil, nil, {scale = 5/6}) object:centerFont() end for _,object in pairs(playerList) do if type(object) == "table" and object.Ref then for i, player in pairs(object.Ref) do if player:hasType(gui.TYPE_TEXT) then player:fitFont(nil, nil, {scale = 5/6}) player:centerFont() end end end end end) end function scoreboard:AddPlayer(name, score, icon) local player = { Name = name, Score = score or 0, Icon = icon, UUID = multi.generate_uuid7(), Add = function(self, amt) self.Score = tostring(tonumber(self.Score) + amt) table.sort(playerList, function(a, b) if a.Score == b.Score then return a.Name < b.Name end return tonumber(a.Score) > tonumber(b.Score) end) scoreboard:RenderPlayer(playerList) ScoreResize() end, } table.insert(playerList, player) table.insert(playerStaticList, player) scoreboard:RenderPlayer(playerList) ScoreResize() return player end local colors = {C_GOLD, C_SILVER, C_BRONZE} local function MapColor(index) if index <=3 and index > 0 then return colors[index] else return C_WHITE end end local add_player = leaderboard:newFrame(5,-5,-10,0,0,1-PLAYER_HEIGHT,1,PLAYER_HEIGHT) local edit_player = leaderboard:newFrame(5,-10,-10,0,0,1-2*PLAYER_HEIGHT,1,PLAYER_HEIGHT) local remove_player = leaderboard:newTextButton("Remove Selected",5,-15,-10,0,0,1-3*PLAYER_HEIGHT,1,PLAYER_HEIGHT) remove_player:setFont(20) remove_player.align = gui.ALIGN_CENTER local embededWatch = {remove_player} scoreUpdater:newThread(function() while true do thread.sleep(.01) for i,v in pairs(embededWatch) do v:centerFont() end end end) local function embedTextEdit(reference, default, but_text, callback) reference.color = C_BORDER_NRM local textbox = reference:newTextBox(default,0,0,0,0,.015,.1,.8,.8) textbox.textColor = C_GOLD textbox.blink = false textbox.color = C_BORDER_TOP textbox.textColor = C_WHITE textbox:OnPressed(function() textbox.text = "" end) local button = reference:newTextButton(but_text,5,0,-10,0,.815,.1,.185,.8) button.color = color.new("#7eae5b") button:OnReleased(function() callback(textbox) end) gui.apply({ setFont = {20}, align = gui.ALIGN_CENTER },textbox,button,reference) table.insert(embededWatch,textbox) table.insert(embededWatch,button) end remove_player.color = color.new("#a13a3a") remove_player:OnReleased(function() local player = GetActivePlayer() uuid = player.UUID for i = 1, #playerList do if playerList[i].UUID == uuid then table.remove(playerList,i) break end end for i = 1, #playerStaticList do if playerStaticList[i].UUID == uuid then table.remove(playerStaticList,i) break end end scoreboard:RenderPlayer(playerList) player.Ref.Frame:destroy() end) embedTextEdit(add_player, "Player Name", "Add", function(self) scoreboard:AddPlayer(self.text, "0") end) embedTextEdit(edit_player, "Modify Score", "Edit", function(self) local player = GetActivePlayer() if player then player.Score = self.text scoreboard:RenderPlayer(playerList) end end) function scoreboard:RenderPlayer(list) for index, player in ipairs(list) do if player.Ref then player.Ref[1].text = player.Name or "" player.Ref[2].text = player.Score or "" player.Ref[3].text = tostring(index) player.Ref.Frame:setDualDim(nil,5*index,nil,nil,nil,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT) player.Ref.Frame.link = player gui.apply({ visibility = 0, drawBorder = false, textColor = MapColor(index) }, unpack(player.Ref)) if activePlayer == nil then activePlayer = player.Ref.Frame end if player.Ref.Frame == activePlayer then player.Ref.Frame.borderColor = C_BORDER_NRM player.Ref.Frame.color = C_ROW_NORM else player.Ref.Frame.borderColor = C_BORDER_TOP player.Ref.Frame.color = C_ROW_TOP end else local playernum, playerName, playerIcon, playerScore, playerLine local playerFrame = leaderboard:newFrame(5,5*index,-10,0,0,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT,1,PLAYER_HEIGHT) playerFrame:OnReleased(function(self) activePlayer = self scoreboard:RenderPlayer(playerList) end) playerFrame:respectHierarchy(false) playerFrame.borderColor = C_BORDER_TOP playerFrame.color = C_ROW_TOP playernum = playerFrame:newTextLabel(index,0,0,0,0,.015,.1,1/8,.8) playernum.align = gui.ALIGN_CENTER if player.Icon ~= nil then playerIcon = playerFrame:newImageLabel(player.Icon,0,0,0,0,.16,.1,.1,.8) playerIcon.square = "h" -- When working with scales squaring is trickier. (h/w) to switch on width or height playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.3,0,2/5,.8) playerName.align = gui.ALIGN_LEFT playerLine = playerFrame:newFrame(0,0,0,0,.3,.8,.69,.07) playerLine.color = C_GOLD else playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.16,0,7/13,.8) playerName.align = gui.ALIGN_LEFT playerLine = playerFrame:newFrame(0,0,0,0,.16,.8,7/13 + 2/7,.07) playerLine.color = C_GOLD end playerScore = playerFrame:newTextLabel(player.Score,0,0,0,0,.71,0,2/7,.8) playerScore.align = gui.ALIGN_CENTER playerLine.drawBorder = false gui.apply({ visibility = 0, drawBorder = false, textColor = MapColor(index) },playernum, playerName, playerScore, playerIcon, playerLine) player.Ref = {playerName, playerScore, playernum, playerIcon, playerLine, Frame = playerFrame} end end end gui.Events.OnResized(ScoreResize) ScoreResize() return scoreboard end return menu.registerMenu("board", Menu)