| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- local tween = require('lib/deps/tween/tween')
- local g = love.graphics
- MenuOptions = class()
- function MenuOptions:init()
- self.geometry = setmetatable({}, {__index = function(t, k)
- return rawset(t, k, self.geometryFunctions[k]())[k]
- end})
- self.geometryFunctions = {
- options = function()
- local res = {labels = {}, controls = {}}
- local u, v = ctx.u, ctx.v
- local width = self.width * u
- local x = u + self.offset
- local y = .1 * v
- local headerFont = g.setFont('mesmerize', .03 * v)
- local padding = v * .055
- for i = 1, #self.controlGroups do
- local group = self.controlGroups[i]
- local str = group:capitalize()
- table.insert(res.labels, {str, x + width - padding - headerFont:getWidth(str), y})
- y = y + v * .08
- for j = 1, #self.controls[group] do
- local control = self.controls[group][j]
- local radius = .014 * v
- if self.controlTypes[control] == Checkbox then
- res.controls[control] = {x + padding, y, radius}
- elseif self.controlTypes[control] == Dropdown then
- res.controls[control] = {x + padding - radius - 2, y - v * .02, u * .22, v * .04}
- elseif self.controlTypes[control] == Slider then
- res.controls[control] = {x + padding, y, u * .15, radius}
- end
- y = y + v * .06
- end
- end
- res.height = math.max(y, v)
- return res
- end
- }
- self.controlGroups = {'graphics', 'sound', 'gameplay'}
- self.controls = {
- graphics = {'resolution', 'display', 'vsync', 'msaa', 'textureSmoothing', 'postprocessing', 'particles'},
- sound = {'mute', 'master', 'music', 'sound'},
- gameplay = {'colorblind', 'powersave', 'offline'}
- }
- self.controlTypes = {
- resolution = Dropdown,
- display = Dropdown,
- vsync = Checkbox,
- msaa = Checkbox,
- textureSmoothing = Checkbox,
- postprocessing = Checkbox,
- particles = Checkbox,
- mute = Checkbox,
- master = Slider,
- music = Slider,
- sound = Slider,
- colorblind = Checkbox,
- powersave = Checkbox,
- offline = Checkbox
- }
- self.controlLabels = {
- display = 'Monitor',
- msaa = 'Antialiasing',
- textureSmoothing = 'Texture Smoothing',
- colorblind = 'Colorblind Mode',
- powersave = 'Power Saving',
- offline = 'Offline Mode'
- }
- self.controlDescriptions = {
- display = 'Which monitor Muju Juju runs on',
- postprocessing = 'Cool effects like bloom and distortions',
- textureSmoothing = 'Reduces rendering artifacts, especially on smaller screens',
- powersave = 'If you are on a laptop, this will intelligently limit the framerate so Muju Juju doesn\'t kill your battery.',
- offline = 'Muju Juju won\'t ever send or load highscores.'
- }
- self.sliderData = {
- master = {0.0, 1.0, 0.05},
- music = {0.0, 1.0, 0.05},
- sound = {0.0, 1.0, 0.05}
- }
- -- Generate dropdown choices
- self.dropdownChoices = {
- resolution = {},
- display = {}
- }
- self:setMode()
- local resolutions = love.window.getFullscreenModes()
- table.sort(resolutions, function(a, b) return a.width * a.height > b.width * b.height end)
- for i = 1, #resolutions do
- self.dropdownChoices.resolution[i] = resolutions[i].width .. ' x ' .. resolutions[i].height
- end
- for i = 1, love.window.getDisplayCount() do
- table.insert(self.dropdownChoices.display, love.window.getDisplayName(i))
- end
- -- Called at load and when an option is changed externally so components can refresh their state.
- self.refreshControls = function()
- local translators = {
- resolution = function(t)
- if t then return t[1] .. ' x ' .. t[2] end
- return self.dropdownChoices.resolution[1]
- end,
- display = function(index)
- return love.window.getDisplayName(index)
- end,
- msaa = function(x) return x and x > 0 or false end
- }
- table.each(self.controlGroups, function(group)
- table.each(self.controls[group], function(control)
- local val = ctx.options[control]
- self.components[control].value = translators[control] and translators[control](val) or val
- end)
- end)
- end
- -- Called when a component changes its value.
- self.refreshOptions = function(keyChanged, value)
- local translators = {
- resolution = function(str)
- local w, h = str:match('(%d+)%sx%s(%d+)')
- return {w, h}
- end,
- display = function(str)
- for i = 1, love.window.getDisplayCount() do
- if love.window.getDisplayName(i) == str then return i end
- end
- return 1
- end,
- msaa = function(value)
- return value and 4 or 0
- end
- }
- if table.eq(translators[keyChanged] and translators[keyChanged](value) or value, ctx.options[keyChanged]) then return end
- ctx.options[keyChanged] = translators[keyChanged] and translators[keyChanged](value) or value
- if keyChanged == 'resolution' or keyChanged == 'display' or keyChanged == 'vsync' or keyChanged == 'msaa' then
- self:setMode()
- elseif keyChanged == 'mute' then
- ctx.sound:setMute(value)
- elseif keyChanged == 'master' or keyChanged == 'music' or keyChanged == 'sound' then
- ctx.sound.volumes[keyChanged] = value
- ctx.sound:refreshVolumes()
- ctx.sound:play('juju1', function(sound) sound:setPitch(.5 + value / 2) end)
- end
- saveOptions(ctx.options)
- end
- self.components = {}
- table.each(self.controlGroups, function(group)
- table.each(self.controls[group], function(control)
- local value = ctx.options[control]
- local component = ctx.gooey:add(self.controlTypes[control] or Checkbox, control, {value = value})
- component.geometry = function() return self.geometry.options.controls[control] end
- component.getOffset = function() return 0, self.scroll end
- component.label = self.controlLabels[control] or control:capitalize()
- component:on('change', function() self.refreshOptions(control, component.value) end)
- if isa(component, Dropdown) then
- component.choices = self.dropdownChoices[control]
- elseif isa(component, Slider) then
- component.min, component.max, component.round = unpack(self.sliderData[control])
- end
- self.components[control] = component
- end)
- end)
- self.refreshControls()
- self.active = false
- self.offset = 0
- self.tweenDuration = .25
- self.width = .35
- self.offsetTween = tween.new(self.tweenDuration, self, {offset = 0}, 'outBack')
- self.targetScroll = 0
- self.prevTargetScroll = self.targetScroll
- self.scroll = self.targetScroll
- self.height = 10000
- self.canvas = g.newCanvas((self.width + .05) * ctx.u, ctx.v)
- self.tooltipFactor = 0
- self.prevTooltipFactor = self.tooltipFactor
- self.tooltipText = ''
- end
- function MenuOptions:update()
- local u, v = ctx.u, ctx.v
- local mx, my = love.mouse.getPosition()
- self.prevScroll = self.scroll
- self.prevTooltipFactor = self.tooltipFactor
- local joysticks = love.joystick.getJoysticks()
- if #joysticks == 0 then
- if self.targetScroll < 0 then self.targetScroll = lume.lerp(self.targetScroll, 0, math.min(12 * ls.tickrate, 1))
- elseif self.targetScroll > self.height - v then self.targetScroll = lume.lerp(self.targetScroll, self.height - v, math.min(12 * ls.tickrate, 1)) end
- else
- if self.targetScroll < 0 then self.targetScroll = 0
- elseif self.targetScroll > self.height -v then self.targetScroll = self.height - v end
- end
- local dirty = false
- table.each(self.controlGroups, function(group)
- table.each(self.controls[group], function(control)
- local ox, oy = self.components[control]:getOffset()
- local mx, my = mx + ox, my + oy
- if self.controlDescriptions[control] and self.components[control]:contains(mx, my) and ctx.gooey.focused ~= self.components[control] and (not ctx.gooey.focused or not ctx.gooey.focused:contains(mx, my)) then
- self.tooltipFactor = lume.lerp(self.tooltipFactor, 1, math.min(6 * ls.tickrate, 1))
- self.tooltipText = self.controlDescriptions[control]
- dirty = true
- end
- end)
- end)
- if not dirty then
- self.tooltipFactor = lume.lerp(self.tooltipFactor, 0, math.min(1 * ls.tickrate, 1))
- end
- end
- function MenuOptions:draw()
- self.offsetTween:update(ls.dt)
- local u, v = ctx.u, ctx.v
- local mx, my = love.mouse.getPosition()
- if self.offset > -1 then return end
- if self.offsetTween.clock < self.tweenDuration or self.offset < self.width * u then
- table.clear(self.geometry)
- self.height = self.geometry.options.height
- end
- self.scroll = lume.lerp(self.scroll, self.targetScroll, 8 * ls.dt)
- local scroll = self.scroll
- local x1 = u + self.offset
- local width = self.width * u
- self.canvas:clear(0, 0, 0, 0)
- ctx.workingCanvas:clear(0, 0, 0, 0)
- g.setColor(255, 255, 255)
- g.setCanvas(self.canvas)
- g.draw(ctx.screenCanvas, -u - self.offset, 0)
- g.setCanvas()
- if self.offset / -width > .1 then
- for i = 1, 6 do
- local shader = data.media.shaders.horizontalBlur
- shader:send('amount', 2 / self.canvas:getWidth() * (self.offset / -width))
- g.setShader(shader)
- ctx.workingCanvas:renderTo(function()
- g.draw(self.canvas)
- end)
- shader = data.media.shaders.verticalBlur
- shader:send('amount', 2 / self.canvas:getHeight() * (self.offset / -width))
- g.setShader(shader)
- self.canvas:renderTo(function()
- g.draw(ctx.workingCanvas)
- end)
- end
- g.setShader()
- end
- g.draw(self.canvas, x1, 0)
- g.setColor(0, 0, 0, 180)
- g.rectangle('fill', x1, 0, width + .05 * u, v)
- g.setColor(255, 255, 255, 40)
- local percent = self.scroll / ((self.height == v) and 1 or (self.height - v))
- local height = v / self.height * (v - 4)
- local scrolly = 0 + (percent * (v - height))
- local clamped = math.clamp(scrolly, 4, v - height - 4)
- local dif = math.abs(scrolly - clamped)
- if clamped > scrolly then
- height = height - dif
- elseif clamped < scrolly then
- clamped = clamped + dif
- height = height - dif
- end
- g.rectangle('fill', x1 + width - u * .005 - 1, clamped, u * .005, height)
- g.push()
- g.translate(0, -scroll)
- g.setColor(200, 200, 200)
- g.setFont('mesmerize', .04 * v)
- g.printCenter('Options', x1 + width / 2, .05 * v)
- g.setFont('mesmerize', .03 * v)
- g.setColor(255, 255, 255, 180)
- for i = 1, #self.geometry.options.labels do
- g.print(unpack(self.geometry.options.labels[i]))
- end
- local focused = nil
- table.each(self.components, function(component)
- if not focused and ctx.gooey.focused == component then
- focused = component
- else
- component:draw()
- end
- end)
- if focused then
- focused:draw()
- end
- g.pop()
- if self.tooltipFactor > .01 and self.tooltipText ~= '' then
- local tooltipFactor = lume.lerp(self.prevTooltipFactor, self.tooltipFactor, ls.accum / ls.tickrate)
- tooltipFactor = math.clamp((tooltipFactor - .8) / .2, 0, 1)
- local str = self.tooltipText
- local font = g.setFont('mesmerize', .02 * v)
- local width, lines = font:getWrap(str, .2 * u)
- width = width + .02 * v
- local height = (font:getHeight() * lines) + .02 * v
- local x, y = mx + 8, my + 8
- if x + width > u then x = u - width end
- if y + height > v then y = v - height end
- g.setColor(0, 0, 0, 200 * tooltipFactor)
- g.rectangle('fill', x, y, width, height)
- g.setColor(255, 255, 255, 255 * tooltipFactor)
- g.printf(str, x + .01 * v, y + .01 * v, width)
- end
- end
- function MenuOptions:keyreleased(key)
- if key == 'escape' and self.active then
- self:toggle()
- return true
- end
- end
- function MenuOptions:mousepressed(mx, my, b)
- local u, v = ctx.u, ctx.v
- local x1 = u + self.offset
- local width = self.width * u
- if math.inside(mx, my, x1, 0, width, v) then
- if b == 'wd' then
- self:scrollPane(1)
- elseif b == 'wu' then
- self:scrollPane(-1)
- end
- return true
- end
- end
- function MenuOptions:gamepadpressed(gamepad, button)
- if button == 'b' and self.active then
- self:toggle()
- end
- end
- function MenuOptions:gamepadaxis(gamepad, axis, value)
- if axis == 'righty' then
- self:scrollPane(value)
- end
- end
- function MenuOptions:scrollPane(direction)
- local scrollSpeed = .05
- self.targetScroll = self.targetScroll + (ctx.v * scrollSpeed) * direction
- end
- function MenuOptions:resize()
- self.canvas = g.newCanvas((self.width + .05) * ctx.u, ctx.v)
- if self.active then self.offsetTween = tween.new(self.tweenDuration, self, {offset = -self.width * ctx.u}, 'inBack')
- else self.offsetTween = tween.new(self.tweenDuration, self, {offset = 0}, 'outBack') end
- table.clear(self.geometry)
- end
- function MenuOptions:toggle(force)
- if self.offsetTween.clock < self.tweenDuration then return end
- if self.active then
- self.offsetTween = tween.new(self.tweenDuration, self, {offset = 0}, 'inBack')
- else
- self.offsetTween = tween.new(self.tweenDuration, self, {offset = -self.width * ctx.u}, 'outBack')
- end
- self.active = not self.active
- end
- function MenuOptions:setMode(n)
- n = n or 0
- if n and n > 1 then return end
- if Context.started and not self.active then
- ctx:resize()
- return
- end
- local resolutions = love.window.getFullscreenModes()
- local dw, dh = love.window.getDesktopDimensions()
- local options = table.only(ctx.options, {'display', 'vsync', 'msaa'})
- MenuOptions.pixelScale = love.window and love.window.getPixelScale() or 1
- options.highdpi = true
- if not ctx.options.resolution then
- ctx.options.resolution = {resolutions[1].width, resolutions[1].height}
- end
- if tonumber(ctx.options.resolution[1]) == resolutions[1].width and tonumber(ctx.options.resolution[2]) == resolutions[1].height then
- options.fullscreen = true
- options.fullscreentype = 'desktop'
- else
- options.fullscreen = false
- end
- if love.window.setMode(ctx.options.resolution[1] / MenuOptions.pixelScale, ctx.options.resolution[2] / MenuOptions.pixelScale, options) then
- love.window.setTitle('Muju Juju')
- love.window.setIcon(love.image.newImageData('media/graphics/icon.png'))
- ctx:resize()
- if love.window.getPixelScale() == 2 then
- self:setMode(n + 1)
- end
- else
- print('There was a problem applying the requested window options... PANIC _>_>_>')
- end
- end
|