6 Commits
17 changed files with 742 additions and 120 deletions
+2
View File
@@ -1 +1,3 @@
# jeopardy game
build with `love-build.exe .\main.lua`
+1 -1
View File
@@ -3,7 +3,7 @@ return {
name = 'Jeopardy', -- name of the game for your executable
developer = 'Ryan Ward', -- dev name used in metadata of the file
output = 'bin', -- output location for your game, defaults to $SAVE_DIRECTORY
version = '1.0.4', -- 'version' of your game, used to name the folder in output
version = '1.0.5', -- 'version' of your game, used to name the folder in output
love = '11.5', -- version of LÖVE to use, must match github releases
ignore = {'bin', 'love', 'server_test', '.gitignore', '.gitmodules', 'build.lua', 'login_dialog.yaml', 'package.bat', 'README.md'}, -- folders/files to ignore in your project
icon = 'assets/icon.png', -- 256x256px PNG icon for game, will be converted for you
+1 -1
Submodule gui updated: 27995af07f...76abe31426
+404 -7
View File
@@ -24,8 +24,6 @@ log = logger.new("jeopardy", {
local gui = require("gui")
local color = require("gui.core.color")
local title = require("menus.title")
local menu = require("gui.core.menus")
color.indexColor("bg","#2b348b")
color.indexColor("tile_bg","#5459e4")
@@ -41,6 +39,27 @@ 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")
@@ -52,17 +71,377 @@ 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
-- 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"))
@@ -71,6 +450,23 @@ function love.load()
-- 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
@@ -95,6 +491,7 @@ function love.load()
menu.init(bg)
loadLog:debug("Displaying title menu")
title:visible(true)
]]
end
function love.update(dt)
+89 -27
View File
@@ -9,6 +9,14 @@ local menu = require("gui.core.menus")
local transition = require("gui.core.transitions")
local logger = require("utils/logger")
-- Constants
local IMAGE = "image"
local AUDIO = "audio"
local VIDEO = "video"
local TEXT = "text"
local LIST = "list"
local NUMBER = "number"
local timesup = love.audio.newSource("assets/sounds/timesup.mp3", "static")
local double = love.audio.newSource("assets/sounds/double.mp3", "static")
@@ -37,6 +45,9 @@ local dd_count = 0
local dd_enabled = false
local applied_dd = false
local board, question
local visibility = 1
local headerVis = .9
local footerVis = .9
local function cleanUp(frameRef)
boardLog:debug("Cleaning up state and returning to title screen")
@@ -171,6 +182,12 @@ function LoadTemplate(name, path)
ALIGN_LEFT = gui.ALIGN_LEFT,
ALIGN_RIGHT = gui.ALIGN_RIGHT,
ALIGN_JUSTIFY = gui.ALIGN_JUSTIFY,
IMAGE = IMAGE,
AUDIO = AUDIO,
VIDEO = VIDEO,
TEXT = TEXT,
LIST = LIST,
NUMBER = NUMBER,
-- Global vars
color = color,
error = multi.error,
@@ -182,6 +199,7 @@ function LoadTemplate(name, path)
-- love stuff
newSource = love.audio.newSource,
log = templateLog,
}
env._G = env
@@ -209,6 +227,15 @@ local function manageCosmetics(frame, cosmetics)
colC = color.shaderColC
})
end
if cosmetics.visibility then
visibility = cosmetics.visibility
end
if cosmetics["header-visibility"] then
headerVis = cosmetics["header-visibility"]
end
if cosmetics["footer-visibility"] then
footerVis = cosmetics["footer-visibility"]
end
end
function gui:cleanup()
@@ -222,6 +249,7 @@ end
function Menu(frame, path, deb)
local frameRef = frame:newImageLabel():fullFrame()
frameRef.visibility = 0
frame.visibility = 0
boardLog:info("Building board")
if path:match(".zip") then
local success = love.filesystem.mount(path, "quiz", true)
@@ -244,8 +272,14 @@ function Menu(frame, path, deb)
end
local scoreboard = ScoreBoard(frameRef, 0, 0, 0, 0, .06, .1, .170, .8)
local properties = Properties(frameRef, 0, 0, 0, 0, .06, .1, .170, .8)
local function showProperties(b)
properties.visible = b
scoreboard.leaderboard.visible = not b
end
showProperties(true)
local qframe = frameRef:newFrame(0, 0, 0, 0, .24, .1, .70, .8)
qframe.visibility = 0
local dframe = frameRef:newTextLabel("",0, 0, 0, 0, .24, .1, .7, .8)
dframe:scaleFont(.075)
@@ -270,7 +304,7 @@ function Menu(frame, path, deb)
refresh:centerFont()
refresh:scaleFont(.9)
refresh.align = gui.ALIGN_CENTER
refresh.color = color.green
refresh.color = color.new("#12a217")
refresh:OnReleased(function()
cleanUp(frameRef)
menu.getMenu("title"):visible(true)
@@ -291,7 +325,7 @@ function Menu(frame, path, deb)
local function displayAnswer(q)
dframe.text = q.answer
dframe.textColor = color.white
dframe.textColor = color.question_font
dframe.visible = true
dframe.click = q["display-mode"] == "click"
@@ -332,6 +366,11 @@ function Menu(frame, path, deb)
local wrong = handler:newTextButton("Wrong", 5,0,-10,0,2/3,.1,1/3,.8)
wrong.color = color.wrong
function SetVisibility()
handler.visibility = footerVis
label.visibility = headerVis
end
gui.apply({
setRoundness = {15,15,60},
centerFont = {},
@@ -376,6 +415,8 @@ function Menu(frame, path, deb)
local data = loader:new(path)
board, question, dailydouble = qframe:newFrame(), qframe:newFrame(), qframe:newImageLabel("assets/images/double.jpg")
board.visibility = 0
question.visibility = 0
index = data.index
local cosmetics = index.cosmetics or {}
@@ -386,12 +427,12 @@ function Menu(frame, path, deb)
cosmetics.colors[v] = color.rgbToHex(color[v])
end
end
frameRef:shaderTime()
manageCosmetics(frameRef, cosmetics)
board:fullFrame()
question:fullFrame()
question.visibility = visibility
dailydouble:fullFrame()
dailydouble.visible = false
@@ -414,6 +455,7 @@ function Menu(frame, path, deb)
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
@@ -428,12 +470,15 @@ function Menu(frame, path, deb)
reset(function(q)
question:cleanup()
manageCosmetics(frameRef, cosmetics)
SetVisibility()
label.text = default_text
if q["display-answer"] then
displayAnswer(q)
end
end)
SetVisibility()
for cat,v in pairs(index.categories) do
local c
if v.image then
@@ -444,6 +489,7 @@ function Menu(frame, path, deb)
c.textColor = color.header_font
c.color = color.header_tile
end
c.visibility = visibility
c.UUID = multi.generate_uuid7() -- Each otpion gets a unique UUID
@@ -469,6 +515,7 @@ function Menu(frame, path, deb)
t.category = v.name
t.index = tier
t.price = start + inc*(tier-1)
t.visibility = visibility
t:OnEnter(function(self)
self:setShader(gui.SHADERS.glow)
end)
@@ -501,7 +548,10 @@ function Menu(frame, path, deb)
end
self.textVisibility = 0
local template = LoadTemplate(q.template, path)
if not template then boardLog:fatal("template returned nil. Likely an invalid template format. Did you return?") end
question.visible = true
board.visible = false
-- question.visibility = visibility
local player = GetActivePlayer()
local tm
local stop
@@ -526,7 +576,7 @@ function Menu(frame, path, deb)
end)
end
local finished = false
template.index(question, q, function(ans, m)
local params = template.index(question, q, function(ans, m)
local m = m or 1
if finished then return end
player = GetActivePlayer()
@@ -538,6 +588,7 @@ function Menu(frame, path, deb)
player:Add((self.price*mul)*m)
finished = true
question.visible = false
board.visible = true
reset:Fire(q)
elseif ans == false then
player:Add(-self.price*mul)
@@ -545,11 +596,13 @@ function Menu(frame, path, deb)
else
finished = true
question.visible = false
board.visible = true
reset:Fire(q)
end -- nil is a valid option where you weren't right or wrong, you just skipped
self.text = ""
end)
boardUpdater:newThread("QuestionUpdater",function()
if not template.update then return end
while true do
template.update(dt)
thread.sleep(.1)
@@ -565,8 +618,6 @@ function Menu(frame, path, deb)
return background
end
function GetActivePlayer()
if not activePlayer then return end
return activePlayer.link
@@ -597,22 +648,9 @@ function love.filedropped(file)
local data = file:read()
print("Load file? " .. file:getFilename())
end
function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
-- Colors
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")
----------------
--- Sideboard
---
local C_BG_PANEL = color.scoreboard_panel_bg
local C_BG_HEADER = color.scoreboard_header_bg
@@ -628,13 +666,37 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
local C_SILVER = color.scoreboard_precious_silver
local C_BRONZE = color.scoreboard_precious_bronze
-- Config stuff
local LEADER_HEIGHT_SCALE = .06
local HEIGHT_SCALE = .03
local PLAYER_HEIGHT = .05
--
local rankColors = { C_GOLD, C_SILVER, C_BRONZE }
function Properties(frame, x, y, w, h, sx, sy, sw, sh)
local prop = frame:newFrame(x, y, w, h, sx, sy, sw, sh)
prop.color = C_BORDER_NRM
local header = prop:newTextLabel("PROPERTIES",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
header:scaleFont(.7)
header:centerFont()
function prop:RenderAttributes(tab)
if tab == nil then
tab = {}
end
if not tab.title then -- make the first option
boardLog:fatal("Templates must define a title attribute")
end
if not tab.answer then -- append to the end
table.insert(tab,{answer=TEXT,description="The answer for the question"})
end
end
return prop
end
-- :newTextLabel(index,0,0,0,0,.015,.1,1/8,.8)
function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
-- Config stuff
local leaderboard = frame:newFrame(x, y, w, h, sx, sy, sw, sh)
leaderboard.color = C_BORDER_NRM
@@ -847,7 +909,7 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
end
end
end
scoreboard.leaderboard = leaderboard
return scoreboard
end
+1 -1
Submodule multi updated: 8b5d10cbb6...43d0cace26
-19
View File
@@ -1,19 +0,0 @@
:: Read the first line of version.txt and assign it to the MY_VERSION variable
for /f "usebackq delims=" %%i in ("version.txt") do (
set "userInput=%%i"
goto :done
)
:done
"C:\Program Files\7-Zip\7z.exe" a -tzip game.love templates version.txt assets multi\integration\loveManager multi\integration\sharedExtensions multi\init.lua utils gui menus conf.lua main.lua
rd /s /q "bin\%userInput%"
if not exist "bin\%userInput%" mkdir "bin\%userInput%"
copy /b love\love.exe + game.love "bin\%userInput%\Jeopardy.exe"
copy /b love\love.dll "bin\%userInput%\love.dll"
copy /b lovel\lua51.dll "bin\%userInput%\lua51.dll"
copy /b love\mpg123.dll "bin\%userInput%\mpg123.dll"
copy /b love\msvcp120.dll "bin\%userInput%\msvcp120.dll"
copy /b love\msvcr120.dll "bin\%userInput%\msvcr120.dll"
copy /b love\OpenAL32.dll "bin\%userInput%\OpenAL32.dll"
copy /b love\SDL2.dll "bin\%userInput%\SDL2.dll"
copy /b love\license.txt "bin\%userInput%\license.txt"
+10 -4
View File
@@ -6,10 +6,10 @@ local plusLabel
local function index(window, q, callback)
frame = window:newFrame(0,0,0,-200,0,.2,1,.8)
frame.visibility = 0
label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2)
label = window:newTextLabel(q.title,0,0,0,0,0,0,1,.2)
label.align = ALIGN_CENTER
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label.drawBorder = false
label:scaleFont(.4)
label:centerFont()
@@ -31,7 +31,7 @@ local function index(window, q, callback)
plusLabel = frame:newImageLabel("assets/images/plus.png", 0, 0, 0, 0, .4125, .4, .175)
plusLabel.square = "w"
plusLabel.align = ALIGN_CENTER
plusLabel.textColor = color.white
plusLabel.textColor = color.question_font
-- plusLabel.visibility=0
-- Right image: takes up 0.58 to 1.0 of width
@@ -44,6 +44,12 @@ local function index(window, q, callback)
}, imageHolder, plusLabel, imageHolder2)
useDefaultHandler(callback)
return {
{title=label},
{imageA=imageHolder, description="First image to display"},
{imageB=imageHolder2, description="Second image to display"},
}
end
local function update(dt)
+11 -9
View File
@@ -20,15 +20,15 @@ local selected
local function index(window, q, callback)
label = window:newTextLabel(" " ..q.title.. " ",noOf(0,0,1,.3))
label.align = ALIGN_CENTER
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label:setFont(50)
label:centerFont()
label:scaleFont(2/3)
label.drawBorder = false
local choices = window:newFrame(noOf(0,.3,1,.7))
choices.color = color.new("#363ac8")
choices.color = color.question_bg
function choices:newChoice(choice, i)
local c = choices:newFrame(noOf(.25,(i-1)/#q.choices,.5,1/#q.choices))
@@ -53,7 +53,7 @@ local function index(window, q, callback)
c.drawBorder = false
local choiceText = c:newTextLabel(choice,noOf(.3,0,1,1))
choiceText.visibility = 0
choiceText.textColor = color.white
choiceText.textColor = color.question_font
-- choiceText.align = window.ALIGN_CENTER
choiceText:setFont(40)
choiceText:centerFont()
@@ -84,13 +84,15 @@ local function index(window, q, callback)
end
end,
},confirm)
end
local function update(dt) -- time in seconds that has passed since
-- Return the expected types
return {
{title=label},
{choices=LIST, description="List of options"},
{answer=NUMBER, description="Answer to multiple choice as a number"}
}
end
return {
index = index,
update = update
index = index
}
+9 -3
View File
@@ -12,10 +12,10 @@ local imageHolder
local function index(window, q, callback)
frame = window:newFrame(0,0,0,0,0,.2,1,.8)
frame.visibility = 0
label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2)
label = window:newTextLabel(q.title,0,0,0,0,0,0,1,.2)
label.align = ALIGN_CENTER
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label:scaleFont(.4)
label:centerFont()
label.drawBorder = false
@@ -29,6 +29,12 @@ local function index(window, q, callback)
imageHolder:setDualDim(0,0,imageHolder.imageWidth,imageHolder.imageHeight)
useDefaultHandler(callback)
-- Return the expected types
return {
{title=label},
{image=imageHolder, description="Image the question is about"}
}
end
local function update(dt) -- time in seconds that has passed since
+6 -9
View File
@@ -6,26 +6,23 @@
callback true/false correct/wrong
]]
local label
local function index(window, q, callback)
label = window:newTextLabel(q.title)
local label = window:newTextLabel(q.title)
label.visibility = 0--q.cosmetics.visibility
label.align = ALIGN_CENTER
label:fullFrame()
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label:centerFont()
label:scaleFont(.075)
label.drawBorder = false
useDefaultHandler(callback)
end
local function update(dt) -- time in seconds that has passed since
return {title=label}
end
return {
index = index,
update = update
index = index
}
+10 -5
View File
@@ -15,18 +15,17 @@ local function index(window, q, callback)
callback()
return
end
frame = window:newFrame(0,0,0,0,0,.2,1,.8)
local frame = window:newFrame(0,0,0,0,0,.2,1,.8)
frame.visibility = 0
label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2)
label.align = ALIGN_CENTER
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label:scaleFont(.4)
label:centerFont()
label.drawBorder = false
imageHolder = frame:newImageButton("assets/images/speaker.png",0,0,0,0,0,0,.3,.3)
imageHolder = window:newImageButton("assets/images/speaker.png",0,0,0,0,0,0,.3,.3)
imageHolder.scale = "h"
imageHolder:setAspectSize(imageHolder.imageWidth,imageHolder.imageHeight)
imageHolder:centerX(true)
imageHolder:centerY(true)
local sound = newSource(q.sound,"stream")
@@ -44,6 +43,12 @@ local function index(window, q, callback)
end)
useDefaultHandler(callback)
-- Return the expected types
return {
{title=label},
{sound=AUDIO, description="Sound that will be played"}
}
end
local function update(dt) -- time in seconds that has passed since
+9 -3
View File
@@ -15,12 +15,12 @@ local function index(window, q, callback)
callback()
return
end
frame = window:newFrame(0,0,0,-200,0,.2,1,.8)
local frame = window:newFrame(0,0,0,-200,0,.2,1,.8)
frame.visibility = 0
label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2)
label.align = ALIGN_CENTER
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label:centerFont()
label:scaleFont(.4)
label.drawBorder = false
@@ -36,6 +36,12 @@ local function index(window, q, callback)
video.XButton.visible = false
useDefaultHandler(callback)
-- Return the expected types
return {
{title=label},
{video=VIDEO, description="Video that will be played"}
}
end
local function update(dt) -- time in seconds that has passed since
+12 -5
View File
@@ -6,16 +6,15 @@
callback true/false correct/wrong
]]
local label
local imageHolder
local function index(window, q, callback)
frame = window:newFrame(0,0,0,0,0,.2,1,.8)
local frame = window:newFrame(0,0,0,0,0,.2,1,.8)
frame.visibility = 0
label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2)
local label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2)
label.align = ALIGN_CENTER
label.textColor = color.white
label.color = color.new("#363ac8")
label.textColor = color.question_font
label.color = color.question_bg
label:scaleFont(.4)
label:centerFont()
label.drawBorder = false
@@ -73,6 +72,14 @@ local function index(window, q, callback)
end)
useDefaultHandler(callback)
-- Return the expected types
return {
{title=label},
{zoom=IMAGE,description = "Full zoom in on an image"},
{minzoom=IMAGE,description = "Minimum zoom before the full image is revealed"},
{fullimage=IMAGE,description = "Full image to show"}
}
end
local function update(dt) -- time in seconds that has passed since
+151
View File
@@ -0,0 +1,151 @@
--[[
file_picker.lua
Cross-platform native file picker for LuaJIT (Windows + Linux).
Windows: uses the native Win32 comdlg32.dll GetOpenFileNameA API via FFI.
No external dependencies required.
Linux: there is no single native "file picker" API — shells out to
whichever dialog tool is installed (zenity, kdialog, or yad).
Install one of these:
sudo apt install zenity # GNOME/GTK
sudo apt install kdialog # KDE
sudo apt install yad # lightweight alternative
Usage:
local picker = require("file_picker")
local path = picker.open_file{ title = "Select a file", filter = "*.txt" }
]]
local ffi = require("ffi")
local M = {}
-- ---------------------------------------------------------------------
-- Windows implementation
-- ---------------------------------------------------------------------
local function win_open_file(opts)
opts = opts or {}
ffi.cdef[[
typedef struct {
unsigned long lStructSize;
void* hwndOwner;
void* hInstance;
const char* lpstrFilter;
char* lpstrCustomFilter;
unsigned long nMaxCustFilter;
unsigned long nFilterIndex;
char* lpstrFile;
unsigned long nMaxFile;
char* lpstrFileTitle;
unsigned long nMaxFileTitle;
const char* lpstrInitialDir;
const char* lpstrTitle;
unsigned long Flags;
unsigned short nFileOffset;
unsigned short nFileExtension;
const char* lpstrDefExt;
long lCustData;
void* lpfnHook;
const char* lpTemplateName;
void* pvReserved;
unsigned long dwReserved;
unsigned long FlagsEx;
} OPENFILENAMEA;
int GetOpenFileNameA(OPENFILENAMEA* lpofn);
int GetSaveFileNameA(OPENFILENAMEA* lpofn);
]]
local comdlg32 = ffi.load("comdlg32")
local OFN_EXPLORER = 0x00080000
local OFN_FILEMUSTEXIST = 0x00001000
local OFN_PATHMUSTEXIST = 0x00000800
local MAX_PATH = 32768 -- generous buffer for long paths
local file_buf = ffi.new("char[?]", MAX_PATH)
-- Filter must be a "\0"-delimited, double-"\0"-terminated string,
-- e.g. "Text Files\0*.txt\0All Files\0*.*\0\0"
local filter = opts.filter or "All Files\0*.*\0\0"
local ofn = ffi.new("OPENFILENAMEA")
ofn.lStructSize = ffi.sizeof("OPENFILENAMEA")
ofn.lpstrFile = file_buf
ofn.nMaxFile = MAX_PATH
ofn.lpstrFilter = filter
ofn.lpstrTitle = opts.title or "Select a file"
ofn.Flags = OFN_EXPLORER + OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST
local picker_fn = opts.save and comdlg32.GetSaveFileNameA or comdlg32.GetOpenFileNameA
local ok = picker_fn(ofn)
if ok ~= 0 then
return ffi.string(file_buf)
end
return nil -- user cancelled or an error occurred
end
-- ---------------------------------------------------------------------
-- Linux implementation
-- ---------------------------------------------------------------------
local function shell_escape(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
local function try_command(cmd)
local handle = io.popen(cmd)
if not handle then return nil end
local result = handle:read("*l")
local ok = handle:close()
if ok and result and #result > 0 then
return result
end
return nil
end
local function linux_open_file(opts)
opts = opts or {}
local title = shell_escape(opts.title or "Select a file")
local save = opts.save
local candidates = {
save
and ("zenity --file-selection --save --confirm-overwrite --title=%s 2>/dev/null"):format(title)
or ("zenity --file-selection --title=%s 2>/dev/null"):format(title),
save
and ("kdialog --getsavefilename . --title %s 2>/dev/null"):format(title)
or ("kdialog --getopenfilename . --title %s 2>/dev/null"):format(title),
save
and ("yad --file-selection --save --title=%s 2>/dev/null"):format(title)
or ("yad --file-selection --title=%s 2>/dev/null"):format(title),
}
for _, cmd in ipairs(candidates) do
local result = try_command(cmd)
if result then return result end
end
return nil, "No supported dialog tool found (install zenity, kdialog, or yad)"
end
-- ---------------------------------------------------------------------
-- Public API
-- ---------------------------------------------------------------------
--- Opens a native file picker dialog.
-- @param opts table: { title = string, filter = string (Windows only), save = bool }
-- @return path (string) or nil, err
function M.open_file(opts)
if ffi.os == "Windows" then
return win_open_file(opts)
elseif ffi.os == "Linux" then
return linux_open_file(opts)
else
return nil, "Unsupported OS: " .. ffi.os
end
end
return M
+4 -4
View File
@@ -8,10 +8,10 @@ local function startTimer(opt)
autoText = true,
autoColor = true,
autoCleanup = false,
startColor = color.green,
textColor = color.black,
warnColor = color.yellow,
timeColor = color.red,
startColor = color.timer_start,
textColor = color.timer_font,
warnColor = color.timer_warn,
timeColor = color.timer_end,
finegrained = false,
visibility = 1
}
+1 -1
View File
@@ -1 +1 @@
1.0.4
1.0.5