local logger = require("utils/logger") local lovehandler = require("utils/love_file_handler") local zip = require("utils/zip") local multi, thread = require("multi"):init({priority=true}) multi.setClock(require("socket").gettime) -- When on linux os.clock doesn't reture actual seconds the program has elapsed for local GLOBAL, THREAD = require("multi.integration.loveManager"):init() local fileHandler, logCtrl = lovehandler.new("logs/jeopardy.log", { max_size = 512 * 1024, -- rotate at 512 KB max_files = 3, buffered = false, -- write every line immediately (safest for games) }) lovehandler.auto_flush_on_quit(logCtrl) log = logger.new("jeopardy", { level = logger.LEVELS.INFO, handlers = { logger.handlers.ConsoleHandler({ colours = true }), fileHandler, }, }) local gui = require("gui") local color = require("gui.core.color") color.indexColor("bg","#2b348b") color.indexColor("tile_bg","#5459e4") color.indexColor("header_tile","#2a2e91") color.indexColor("status_font", "#e4b82a") color.indexColor("title_font","#b16d24") color.indexColor("question_bg","#363ac8") color.indexColor("tile_font","#ffffff") color.indexColor("header_font","#fef6eeda") color.indexColor("correct", "#52b11b") color.indexColor("skip", "#5d5d5d") color.indexColor("wrong", "#bd2626") color.indexColor("shaderColA","rgb01(0.10, 0.12, 0.38)") color.indexColor("shaderColB","rgb01(0.20, 0.22, 0.58)") color.indexColor("shaderColC","rgb01(0.55, 0.58, 0.78)") color.indexColor("scoreboard_panel_bg", "#1a1a2e") color.indexColor("scoreboard_header_bg", "#16213e") color.indexColor("scoreboard_accent_color", "#e94560") color.indexColor("scoreboard_row_top_bg", "#1c2641") color.indexColor("scoreboard_row_norm_bg", "#121226") color.indexColor("scoreboard_border_top_color", "#323c6e") color.indexColor("scoreboard_border_nrm_color", "#1c1c37") color.indexColor("scoreboard_bar_empty_color", "#232341") color.indexColor("scoreboard_text_muted_color", "#786e96") color.indexColor("scoreboard_pure_white", "#ffffff") color.indexColor("scoreboard_precious_gold", "#ffd700") color.indexColor("scoreboard_precious_silver", "#C0C0C0") color.indexColor("scoreboard_precious_bronze", "#cd7f32") color.indexColor("timer_start","#12a217") color.indexColor("timer_font","#000000") color.indexColor("timer_warn","#ffff14") color.indexColor("timer_end","#e50000") color.indexColor("question_font","rgb(255,255,255)") local title = require("menus.title") local menu = require("gui.core.menus") require("gui.addons") gui:setHotKey({"f11"})(function() fullscreen, fstype = love.window.getFullscreen() love.window.setFullscreen(not fullscreen, "desktop") end) local loadLog = log:child("load") local old = multi.error -- multi.error = function(self, err) -- loadLog:error("Error: %s", err) -- loadLog:error(debug.traceback()) -- love.quit() -- end function readFileData(path) return io.open(path,"r"):read("*a") end local picker = THREAD:newFunction(function() local filepicker = require("utils/filepicker") local path, err = filepicker.open_file{ title = "Select a file" } if path then return path else return nil, "No file selected." .. (err and (" (" .. err .. ")") or "") end end, true) local nof = gui.noOf function gui:newNumberPicker(x, y, w, h, sx, sy, sw, sh, opts) opts = opts or {} opts.value = opts.value or 0 opts.inc = opts.inc or 1 opts.decimals = opts.decimals or 0 -- 0 = integers only -- opts.min / opts.max / opts.onChange are optional, leave nil for "no limit" local picker = self:newFrame(x, y, w, h, sx, sy, sw, sh) picker:setRoundness(6, 6) picker.active = false local leftF = picker:newFrame(0, 0, 0, 0, 0, 0, .15, 1) local left = leftF:newTextButton("-", 0, 0, 0, 0, 0, 0, 1, 1) local rightF = picker:newFrame(0, 0, 0, 0, .85, 0, .15, 1) local right = rightF:newTextButton("+", 0, 0, 0, 0, 0, 0, 1, 1) local textbox = picker:newTextBox("", 0, 0, 0, 0, .15, 0, .7, 1) textbox.blink = false gui.apply({ scaleFont = {.45}, centerFont = {}, run = {function(self) if self.text == "-" or self.text == "+" then self:centerX(true) self:centerY(true) -- also center vertically, not just horizontally end end}, align = gui.ALIGN_CENTER, }, textbox, left, right) gui.apply({ visibility = 0, drawBorder = false, -- flat, no boxy outline around the arrows }, left, right, leftF, rightF) -- ---- value handling ---------------------------------------------- local function clamp(n) if opts.min and n < opts.min then n = opts.min end if opts.max and n > opts.max then n = opts.max end return n end local function round(n) if opts.decimals > 0 then local mult = 10 ^ opts.decimals return math.floor(n * mult + 0.5) / mult end return math.floor(n + 0.5) end local function format(n) if opts.decimals > 0 then return string.format("%." .. opts.decimals .. "f", n) end return tostring(math.floor(n)) end local function updateButtons() local canDec = (opts.min == nil) or (opts.value > opts.min) local canInc = (opts.max == nil) or (opts.value < opts.max) left.active, right.active = canDec, canInc left.textColor = canDec and {0, 0, 0, 1} or {.6, .6, .6, 1} right.textColor = canInc and {0, 0, 0, 1} or {.6, .6, .6, 1} end function picker:setValue(n, silent) n = round(clamp(n)) opts.value = n textbox.text = format(n) textbox.cur_pos = #textbox.text updateButtons() if not silent and opts.onChange then opts.onChange(n) end end function picker:getValue() return opts.value end picker:setValue(opts.value, true) left:OnReleased(function() picker:setValue(opts.value - opts.inc) end) right:OnReleased(function() picker:setValue(opts.value + opts.inc) end) textbox:OnWheelMoved(function(_,dx, dy) if dy > 0 then picker:setValue(opts.value + opts.inc) elseif dy < 0 then picker:setValue(opts.value - opts.inc) end end) -- ---- restrict typing to numbers only ------------------------------- local allowNegative = opts.min == nil or opts.min < 0 local allowDecimal = opts.decimals > 0 local function sanitize(s) local out, seenDot = {}, false for i = 1, #s do local ch = s:sub(i, i) if ch:match("%d") then out[#out + 1] = ch elseif ch == "-" and i == 1 and allowNegative then out[#out + 1] = ch elseif ch == "." and allowDecimal and not seenDot then out[#out + 1] = ch seenDot = true end end return table.concat(out) end local tiRef = gui.Events.OnTextInputed(function(input) if gui:getObjectFocus() ~= textbox then return end local clean = sanitize(textbox.text) if clean ~= textbox.text then textbox.text = clean textbox.cur_pos = math.min(textbox.cur_pos, #clean) end end) local kpRef = gui.Events.OnKeyPressed(function(key) if gui:getObjectFocus() ~= textbox then return end if key == "up" then picker:setValue(opts.value + opts.inc) elseif key == "down" then picker:setValue(opts.value - opts.inc) end end) -- re-normalize (round/clamp/reformat) whatever was typed once they -- hit enter or click away, so "3." or "-" don't linger as invalid text local function commit() picker:setValue(tonumber(textbox.text) or opts.value) end textbox:OnReturn(commit) textbox:OnPressedOuter(commit) textbox:OnDestroy(function() gui.Events.OnTextInputed:Unconnect(tiRef) gui.Events.OnKeyPressed:Unconnect(kpRef) end) return picker end -- ---- HSV <-> RGB helpers ---- local function hsvToRgb(h, s, v) local c = v * s local x = c * (1 - math.abs((h / 60) % 2 - 1)) local m = v - c local r, g, b if h < 60 then r, g, b = c, x, 0 elseif h < 120 then r, g, b = x, c, 0 elseif h < 180 then r, g, b = 0, c, x elseif h < 240 then r, g, b = 0, x, c elseif h < 300 then r, g, b = x, 0, c else r, g, b = c, 0, x end return r + m, g + m, b + m end local function rgbToHsv(r, g, b) local max, min = math.max(r, g, b), math.min(r, g, b) local d = max - min local h = 0 if d ~= 0 then if max == r then h = ((g - b) / d) % 6 elseif max == g then h = (b - r) / d + 2 else h = (r - g) / d + 4 end h = h * 60 end local s = (max == 0) and 0 or (d / max) return h, s, max end local function clampNum(n, lo, hi) return math.max(lo, math.min(hi, n)) end local function newCornerGradient(tl, tr, br, bl) return love.graphics.newMesh({ {0, 0, 0, 0, tl[1], tl[2], tl[3], tl[4]}, {1, 0, 0, 0, tr[1], tr[2], tr[3], tr[4]}, {1, 1, 0, 0, br[1], br[2], br[3], br[4]}, {0, 1, 0, 0, bl[1], bl[2], bl[3], bl[4]}, }, "fan") end local _whiteFade, _blackFade local function getFadeImages() if not _whiteFade then -- opaque white on the left fading to transparent on the right _whiteFade = newCornerGradient( {1,1,1,1}, {1,1,1,0}, {1,1,1,0}, {1,1,1,1} ) -- transparent at top fading to opaque black at bottom _blackFade = newCornerGradient( {0,0,0,0}, {0,0,0,0}, {0,0,0,1}, {0,0,0,1} ) end return _whiteFade, _blackFade end function gui:newColorPicker(x, y, w, h, sx, sy, sw, sh, opts) opts = opts or {} local r0 = (opts.r or 255) / 255 local g0 = (opts.g or 0) / 255 local b0 = (opts.b or 0) / 255 local picker = self:newFrame(x, y, w, h, sx, sy, sw, sh) picker.active = false -- container only; don't compete for focus with its children picker.drawBorder = false picker.visibility = 0 local hue, sat, val = rgbToHsv(r0, g0, b0) local hueR, hueG, hueB = hsvToRgb(hue, 1, 1) -- ---- Saturation/Value square ---- local svFrame = picker:newFrame(6, 6, -12, -12, 0, 0, .55, 1) svFrame.drawBorder = false svFrame.visibility = 0 local whiteFade, blackFade = getFadeImages() svFrame.post = function(self) local sx2, sy2, sw2, sh2 = self:getAbsolutes() love.graphics.setColor(hueR, hueG, hueB, 1) love.graphics.rectangle("fill", sx2, sy2, sw2, sh2) love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(whiteFade, sx2, sy2, 0, sw2, sh2) love.graphics.setColor(1, 1, 1, .95) love.graphics.draw(blackFade, sx2, sy2, 0, sw2, sh2) love.graphics.setColor(0, 0, 0, .4) love.graphics.rectangle("line", sx2, sy2, sw2, sh2) love.graphics.setColor(1, 1, 1, 1) end local svThumb = svFrame:newFrame(0, 0, 0, 0) svThumb:makeCircle(0, 0, 6) svThumb.color = {1, 1, 1} svThumb.borderColor = {0, 0, 0} svThumb.drawBorder = true -- ---- Hue strip ---- local hueSlider = picker:newFrame(0, 6, 0, -12, .57, 0, .10, 1) -- applyGradient's "horizontal"/"vertical" naming is backwards from what -- you'd expect visually (it names how colors are laid out in the source -- image, not the on-screen direction) -- if this renders sideways for -- you, just flip the string to "vertical". hueSlider:applyGradient("horizontal", {1, 0, 0, 1}, {1, 1, 0, 1}, {0, 1, 0, 1}, {0, 1, 1, 1}, {0, 0, 1, 1}, {1, 0, 1, 1}, {1, 0, 0, 1}) hueSlider.drawBorder = false local hueThumb = hueSlider:newFrame(0, 0, 0, 4, 0, 0, 1, 0) hueThumb.color = {1, 1, 1} hueThumb.borderColor = {0, 0, 0} hueThumb.drawBorder = true -- ---- Preview + numeric fields ---- local preview = picker:newFrame(6, 6, -12, -6, .70, 0, .30, .20) preview:setRoundness(4, 4) local rPicker, gPicker, bPicker local function refresh(fireChange) local r, g, b = hsvToRgb(hue, sat, val) hueR, hueG, hueB = hsvToRgb(hue, 1, 1) preview.color = {r, g, b} local _, _, sw2, sh2 = svFrame:getAbsolutes() svThumb:rawSetDualDim(sat * sw2 - 12, (1 - val) * sh2 - 6) local _, _, _, hh2 = hueSlider:getAbsolutes() hueThumb:rawSetDualDim(nil, (hue / 360) * hh2 - 2) local r255 = math.floor(r * 255 + .5) local g255 = math.floor(g * 255 + .5) local b255 = math.floor(b * 255 + .5) if rPicker then rPicker:setValue(r255, true) gPicker:setValue(g255, true) bPicker:setValue(b255, true) end if fireChange ~= false and opts.onChange then opts.onChange(r255, g255, b255) end end local function onRGBEdited() local r = rPicker:getValue() / 255 local g = gPicker:getValue() / 255 local b = bPicker:getValue() / 255 hue, sat, val = rgbToHsv(r, g, b) refresh(true) end rPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .26, .30, .20, {value = math.floor(r0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited}) gPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .52, .30, .20, {value = math.floor(g0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited}) bPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .78, .30, .20, {value = math.floor(b0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited}) gui.apply({ scaleFont={.1} },rPicker,gPicker,bPicker) -- ---- drag interaction (same press/move/release pattern the built-in -- textbox uses for its own text-selection dragging) ---- local function setSV(mx, my) local sx2, sy2, sw2, sh2 = svFrame:getAbsolutes() local lx = clampNum(mx - sx2, 0, sw2) local ly = clampNum(my - sy2, 0, sh2) sat = sw2 > 0 and (lx / sw2) or 0 val = sh2 > 0 and (1 - ly / sh2) or 0 refresh(true) end local svDragging = false svFrame:OnPressed(function(_, mx, my) svDragging = true; setSV(mx, my) end) svFrame:OnMoved(function(_, mx, my) if svDragging then setSV(mx, my) end end) svFrame:OnReleased(function() svDragging = false end) svFrame:OnReleasedOuter(function() svDragging = false end) local function setHue(mx, my) local _, sy2, _, sh2 = hueSlider:getAbsolutes() local ly = clampNum(my - sy2, 0, sh2) hue = sh2 > 0 and (ly / sh2) * 360 or 0 refresh(true) end local hueDragging = false hueSlider:OnPressed(function(_, mx, my) hueDragging = true; setHue(mx, my) end) hueSlider:OnMoved(function(_, mx, my) if hueDragging then setHue(mx, my) end end) hueSlider:OnReleased(function() hueDragging = false end) hueSlider:OnReleasedOuter(function() hueDragging = false end) -- ---- public API ---- function picker:getColor() local r, g, b = hsvToRgb(hue, sat, val) return math.floor(r * 255 + .5), math.floor(g * 255 + .5), math.floor(b * 255 + .5) end function picker:setColor(r255, g255, b255) hue, sat, val = rgbToHsv(r255 / 255, g255 / 255, b255 / 255) refresh(false) end refresh(false) return picker end function love.load() love.keyboard.setKeyRepeat(true) -- local z = zip.new() -- z:add("main.lua",readFileData("main.lua")) -- z:add("package.bat",readFileData("package.bat")) -- local data = z:build() -- local file = io.open("test.zip","wb") -- file:write(data) -- file:close() -- thread:newThread(function() -- local file = picker() -- print("File selected",file) -- end).OnError(print) -- local numpicker = gui:newNumberPicker(0, 0, 200, 40, .5, .5, 0, 0, { -- value = 1, inc = 1, min = 1, max = 4, -- onChange = function(v) print("value is now", v) end, -- }) local frame = gui:newFrame(0, 0, 500, 250, .5, .5, 0, 0) local colorpicker = frame:newColorPicker(0, 0, 0, 0, 0, 0, 1, 1, { r = 255, g = 100, b = 50, onChange = function(r, g, b) print("color:", r, g, b) end, }) --[[ -- bootstrap loadLog:info("Starting Jeopardy Game") loadLog:debug("Setting game identity to jeopardy") for i,v in ipairs(color.listColors(showAllColors)) do loadLog:debug("Indexed color %s",v) end love.filesystem.setIdentity("jeopardy") loadLog:trace("Creating save directory") success = love.filesystem.createDirectory("") if not success then loadLog:fatal("Unable to create save directory") love.quit() end loadLog:trace("Creating save directory") gui:cacheImage({"assets/images/checked.png","assets/images/unchecked.png","assets/images/speaker.png"}) gui:setAspectSize(1920, 1080) gui.aspect_ratio = true loadLog:debug("Creating main background image") local bg = gui:newImageLabel("assets/images/background.png") bg:fullFrame() loadLog:debug("Init menu module") menu.init(bg) loadLog:debug("Displaying title menu") title:visible(true) ]] end function love.update(dt) gui.update(dt) multi:uManager(dt) end function love.draw() gui.draw() end -- custom shaders gui.NewShader("background", [[ extern float time; extern vec2 size; extern vec3 colA; extern vec3 colB; extern vec3 colC; // Hash function (pseudo-random from 2D input) float hash(vec2 p) { p = fract(p * vec2(127.1, 311.7)); p += dot(p, p + 19.19); return fract(p.x * p.y); } // Smooth value noise float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); // Quintic smoothstep vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } // Fractional Brownian Motion – layers of noise at different scales float fbm(vec2 p) { float value = 0.0; float amplitude = 0.5; float frequency = 1.0; for (int i = 0; i < 6; i++) { value += amplitude * noise(p * frequency); frequency *= 2.0; amplitude *= 0.5; } return value; } vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord) { // Normalized UV with aspect ratio correction vec2 uv = screenCoord / size; // Slow, asymmetric time offsets per axis for lava-lamp feel float t1 = time * 0.008; float t2 = time * 0.005; // Domain-warped FBM: feed FBM output back into itself // This creates the bulging, self-folding blob shapes. vec2 q = vec2( fbm(uv + vec2(0.0, 0.0) + t1), fbm(uv + vec2(5.2, 1.3) + t2) ); vec2 r = vec2( fbm(uv + 4.0 * q + vec2(1.7, 9.2) + t1 * 0.7), fbm(uv + 4.0 * q + vec2(8.3, 2.8) + t2 * 0.9) ); float n = fbm(uv + 4.0 * r + t1 * 0.4); vec3 col = mix(colA, colB, clamp(n * 2.0, 0.0, 1.0)); col = mix(col, colC, clamp(n * 2.0 - 0.6, 0.0, 1.0)); return vec4(col, 1.0); } ]])