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("menus.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") boardUpdater.Start() 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 function gui:cleanup() for i = #self.children, 1, -1 do self.children[i]:destroy() end self.children = {} completed_questions = completed_questions + 1 end 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 print(i,v) v.isDouble = true end applied_dd = true end end local qUpdater = gui:getProcessor():newProcessor("question-updater") qUpdater.Start() function LoadTemplate(name, path) if love.filesystem.getInfo("templates/" .. name .. ".lua") then data = love.filesystem.read("templates/" .. name .. ".lua") elseif love.filesystem.getInfo(path .."/templates/" .. name .. ".lua") then data = love.filesystem.read(path .."/templates/" .. name .. ".lua") 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 function Menu(frame, path) local qframe = frame:newFrame(0, 0, 0, 0, .2, .05, .75, .9) qframe.color = color.new("#060ee9") local scoreboard = ScoreBoard(frame, 0, 0, 0, 0, .015, .05, .170, .9) local data = loader:new(path) board, question, dailydouble = qframe:newFrame(), qframe:newFrame(), qframe:newImageLabel("assets/images/double.jpg") index = data.index board:fullFrame() question:fullFrame() dailydouble:fullFrame() dailydouble.visible = false question.visible = false question.color = color.new("#060ce9") 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 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.new("#ffffff") c.color = color.new("#060ce9") end c.UUID = multi.generate_uuid7() -- Each otpion gets a unique UUID img = c:newImageButton("assets/images/placeholder.jpg") img.visibility = 0 img:OnReleased(function(self) self.visible = false end) img:fullFrame() 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.new("#9b9024") t.align = gui.ALIGN_CENTER t.color = color.new("#060ce9") 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 q = index.categories[cat].questions[self.index] 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 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) if finished then return end player = GetActivePlayer() if tm then tm:Cleanup() end stop = true if ans then player:Add(self.price*mul) finished = true question.visible = false question:cleanup() elseif ans == false then player:Add(-self.price*mul) player = GetNextPlayer() else finished = true question.visible = false question:cleanup() 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 scoreUpdater = gui:getProcessor():newProcessor("score-updater") scoreUpdater.Start() 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, 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)