menuoptions.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. local tween = require('lib/deps/tween/tween')
  2. local g = love.graphics
  3. MenuOptions = class()
  4. function MenuOptions:init()
  5. self.geometry = setmetatable({}, {__index = function(t, k)
  6. return rawset(t, k, self.geometryFunctions[k]())[k]
  7. end})
  8. self.geometryFunctions = {
  9. options = function()
  10. local res = {labels = {}, controls = {}}
  11. local u, v = ctx.u, ctx.v
  12. local width = self.width * u
  13. local x = u + self.offset
  14. local y = .1 * v
  15. local headerFont = g.setFont('mesmerize', .03 * v)
  16. local padding = v * .055
  17. for i = 1, #self.controlGroups do
  18. local group = self.controlGroups[i]
  19. local str = group:capitalize()
  20. table.insert(res.labels, {str, x + width - padding - headerFont:getWidth(str), y})
  21. y = y + v * .08
  22. for j = 1, #self.controls[group] do
  23. local control = self.controls[group][j]
  24. local radius = .014 * v
  25. if self.controlTypes[control] == Checkbox then
  26. res.controls[control] = {x + padding, y, radius}
  27. elseif self.controlTypes[control] == Dropdown then
  28. res.controls[control] = {x + padding - radius - 2, y - v * .02, u * .22, v * .04}
  29. elseif self.controlTypes[control] == Slider then
  30. res.controls[control] = {x + padding, y, u * .15, radius}
  31. end
  32. y = y + v * .06
  33. end
  34. end
  35. res.height = math.max(y, v)
  36. return res
  37. end
  38. }
  39. self.controlGroups = {'graphics', 'sound', 'gameplay'}
  40. self.controls = {
  41. graphics = {'resolution', 'display', 'vsync', 'msaa', 'textureSmoothing', 'postprocessing', 'particles'},
  42. sound = {'mute', 'master', 'music', 'sound'},
  43. gameplay = {'colorblind', 'powersave', 'offline'}
  44. }
  45. self.controlTypes = {
  46. resolution = Dropdown,
  47. display = Dropdown,
  48. vsync = Checkbox,
  49. msaa = Checkbox,
  50. textureSmoothing = Checkbox,
  51. postprocessing = Checkbox,
  52. particles = Checkbox,
  53. mute = Checkbox,
  54. master = Slider,
  55. music = Slider,
  56. sound = Slider,
  57. colorblind = Checkbox,
  58. powersave = Checkbox,
  59. offline = Checkbox
  60. }
  61. self.controlLabels = {
  62. display = 'Monitor',
  63. msaa = 'Antialiasing',
  64. textureSmoothing = 'Texture Smoothing',
  65. colorblind = 'Colorblind Mode',
  66. powersave = 'Power Saving',
  67. offline = 'Offline Mode'
  68. }
  69. self.controlDescriptions = {
  70. display = 'Which monitor Muju Juju runs on',
  71. postprocessing = 'Cool effects like bloom and distortions',
  72. textureSmoothing = 'Reduces rendering artifacts, especially on smaller screens',
  73. powersave = 'If you are on a laptop, this will intelligently limit the framerate so Muju Juju doesn\'t kill your battery.',
  74. offline = 'Muju Juju won\'t ever send or load highscores.'
  75. }
  76. self.sliderData = {
  77. master = {0.0, 1.0, 0.05},
  78. music = {0.0, 1.0, 0.05},
  79. sound = {0.0, 1.0, 0.05}
  80. }
  81. -- Generate dropdown choices
  82. self.dropdownChoices = {
  83. resolution = {},
  84. display = {}
  85. }
  86. self:setMode()
  87. local resolutions = love.window.getFullscreenModes()
  88. table.sort(resolutions, function(a, b) return a.width * a.height > b.width * b.height end)
  89. for i = 1, #resolutions do
  90. self.dropdownChoices.resolution[i] = resolutions[i].width .. ' x ' .. resolutions[i].height
  91. end
  92. for i = 1, love.window.getDisplayCount() do
  93. table.insert(self.dropdownChoices.display, love.window.getDisplayName(i))
  94. end
  95. -- Called at load and when an option is changed externally so components can refresh their state.
  96. self.refreshControls = function()
  97. local translators = {
  98. resolution = function(t)
  99. if t then return t[1] .. ' x ' .. t[2] end
  100. return self.dropdownChoices.resolution[1]
  101. end,
  102. display = function(index)
  103. return love.window.getDisplayName(index)
  104. end,
  105. msaa = function(x) return x and x > 0 or false end
  106. }
  107. table.each(self.controlGroups, function(group)
  108. table.each(self.controls[group], function(control)
  109. local val = ctx.options[control]
  110. self.components[control].value = translators[control] and translators[control](val) or val
  111. end)
  112. end)
  113. end
  114. -- Called when a component changes its value.
  115. self.refreshOptions = function(keyChanged, value)
  116. local translators = {
  117. resolution = function(str)
  118. local w, h = str:match('(%d+)%sx%s(%d+)')
  119. return {w, h}
  120. end,
  121. display = function(str)
  122. for i = 1, love.window.getDisplayCount() do
  123. if love.window.getDisplayName(i) == str then return i end
  124. end
  125. return 1
  126. end,
  127. msaa = function(value)
  128. return value and 4 or 0
  129. end
  130. }
  131. if table.eq(translators[keyChanged] and translators[keyChanged](value) or value, ctx.options[keyChanged]) then return end
  132. ctx.options[keyChanged] = translators[keyChanged] and translators[keyChanged](value) or value
  133. if keyChanged == 'resolution' or keyChanged == 'display' or keyChanged == 'vsync' or keyChanged == 'msaa' then
  134. self:setMode()
  135. elseif keyChanged == 'mute' then
  136. ctx.sound:setMute(value)
  137. elseif keyChanged == 'master' or keyChanged == 'music' or keyChanged == 'sound' then
  138. ctx.sound.volumes[keyChanged] = value
  139. ctx.sound:refreshVolumes()
  140. ctx.sound:play('juju1', function(sound) sound:setPitch(.5 + value / 2) end)
  141. end
  142. saveOptions(ctx.options)
  143. end
  144. self.components = {}
  145. table.each(self.controlGroups, function(group)
  146. table.each(self.controls[group], function(control)
  147. local value = ctx.options[control]
  148. local component = ctx.gooey:add(self.controlTypes[control] or Checkbox, control, {value = value})
  149. component.geometry = function() return self.geometry.options.controls[control] end
  150. component.getOffset = function() return 0, self.scroll end
  151. component.label = self.controlLabels[control] or control:capitalize()
  152. component:on('change', function() self.refreshOptions(control, component.value) end)
  153. if isa(component, Dropdown) then
  154. component.choices = self.dropdownChoices[control]
  155. elseif isa(component, Slider) then
  156. component.min, component.max, component.round = unpack(self.sliderData[control])
  157. end
  158. self.components[control] = component
  159. end)
  160. end)
  161. self.refreshControls()
  162. self.active = false
  163. self.offset = 0
  164. self.tweenDuration = .25
  165. self.width = .35
  166. self.offsetTween = tween.new(self.tweenDuration, self, {offset = 0}, 'outBack')
  167. self.targetScroll = 0
  168. self.prevTargetScroll = self.targetScroll
  169. self.scroll = self.targetScroll
  170. self.height = 10000
  171. self.canvas = g.newCanvas((self.width + .05) * ctx.u, ctx.v)
  172. self.tooltipFactor = 0
  173. self.prevTooltipFactor = self.tooltipFactor
  174. self.tooltipText = ''
  175. end
  176. function MenuOptions:update()
  177. local u, v = ctx.u, ctx.v
  178. local mx, my = love.mouse.getPosition()
  179. self.prevScroll = self.scroll
  180. self.prevTooltipFactor = self.tooltipFactor
  181. local joysticks = love.joystick.getJoysticks()
  182. if #joysticks == 0 then
  183. if self.targetScroll < 0 then self.targetScroll = lume.lerp(self.targetScroll, 0, math.min(12 * ls.tickrate, 1))
  184. elseif self.targetScroll > self.height - v then self.targetScroll = lume.lerp(self.targetScroll, self.height - v, math.min(12 * ls.tickrate, 1)) end
  185. else
  186. if self.targetScroll < 0 then self.targetScroll = 0
  187. elseif self.targetScroll > self.height -v then self.targetScroll = self.height - v end
  188. end
  189. local dirty = false
  190. table.each(self.controlGroups, function(group)
  191. table.each(self.controls[group], function(control)
  192. local ox, oy = self.components[control]:getOffset()
  193. local mx, my = mx + ox, my + oy
  194. 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
  195. self.tooltipFactor = lume.lerp(self.tooltipFactor, 1, math.min(6 * ls.tickrate, 1))
  196. self.tooltipText = self.controlDescriptions[control]
  197. dirty = true
  198. end
  199. end)
  200. end)
  201. if not dirty then
  202. self.tooltipFactor = lume.lerp(self.tooltipFactor, 0, math.min(1 * ls.tickrate, 1))
  203. end
  204. end
  205. function MenuOptions:draw()
  206. self.offsetTween:update(ls.dt)
  207. local u, v = ctx.u, ctx.v
  208. local mx, my = love.mouse.getPosition()
  209. if self.offset > -1 then return end
  210. if self.offsetTween.clock < self.tweenDuration or self.offset < self.width * u then
  211. table.clear(self.geometry)
  212. self.height = self.geometry.options.height
  213. end
  214. self.scroll = lume.lerp(self.scroll, self.targetScroll, 8 * ls.dt)
  215. local scroll = self.scroll
  216. local x1 = u + self.offset
  217. local width = self.width * u
  218. self.canvas:clear(0, 0, 0, 0)
  219. ctx.workingCanvas:clear(0, 0, 0, 0)
  220. g.setColor(255, 255, 255)
  221. g.setCanvas(self.canvas)
  222. g.draw(ctx.screenCanvas, -u - self.offset, 0)
  223. g.setCanvas()
  224. if self.offset / -width > .1 then
  225. for i = 1, 6 do
  226. local shader = data.media.shaders.horizontalBlur
  227. shader:send('amount', 2 / self.canvas:getWidth() * (self.offset / -width))
  228. g.setShader(shader)
  229. ctx.workingCanvas:renderTo(function()
  230. g.draw(self.canvas)
  231. end)
  232. shader = data.media.shaders.verticalBlur
  233. shader:send('amount', 2 / self.canvas:getHeight() * (self.offset / -width))
  234. g.setShader(shader)
  235. self.canvas:renderTo(function()
  236. g.draw(ctx.workingCanvas)
  237. end)
  238. end
  239. g.setShader()
  240. end
  241. g.draw(self.canvas, x1, 0)
  242. g.setColor(0, 0, 0, 180)
  243. g.rectangle('fill', x1, 0, width + .05 * u, v)
  244. g.setColor(255, 255, 255, 40)
  245. local percent = self.scroll / ((self.height == v) and 1 or (self.height - v))
  246. local height = v / self.height * (v - 4)
  247. local scrolly = 0 + (percent * (v - height))
  248. local clamped = math.clamp(scrolly, 4, v - height - 4)
  249. local dif = math.abs(scrolly - clamped)
  250. if clamped > scrolly then
  251. height = height - dif
  252. elseif clamped < scrolly then
  253. clamped = clamped + dif
  254. height = height - dif
  255. end
  256. g.rectangle('fill', x1 + width - u * .005 - 1, clamped, u * .005, height)
  257. g.push()
  258. g.translate(0, -scroll)
  259. g.setColor(200, 200, 200)
  260. g.setFont('mesmerize', .04 * v)
  261. g.printCenter('Options', x1 + width / 2, .05 * v)
  262. g.setFont('mesmerize', .03 * v)
  263. g.setColor(255, 255, 255, 180)
  264. for i = 1, #self.geometry.options.labels do
  265. g.print(unpack(self.geometry.options.labels[i]))
  266. end
  267. local focused = nil
  268. table.each(self.components, function(component)
  269. if not focused and ctx.gooey.focused == component then
  270. focused = component
  271. else
  272. component:draw()
  273. end
  274. end)
  275. if focused then
  276. focused:draw()
  277. end
  278. g.pop()
  279. if self.tooltipFactor > .01 and self.tooltipText ~= '' then
  280. local tooltipFactor = lume.lerp(self.prevTooltipFactor, self.tooltipFactor, ls.accum / ls.tickrate)
  281. tooltipFactor = math.clamp((tooltipFactor - .8) / .2, 0, 1)
  282. local str = self.tooltipText
  283. local font = g.setFont('mesmerize', .02 * v)
  284. local width, lines = font:getWrap(str, .2 * u)
  285. width = width + .02 * v
  286. local height = (font:getHeight() * lines) + .02 * v
  287. local x, y = mx + 8, my + 8
  288. if x + width > u then x = u - width end
  289. if y + height > v then y = v - height end
  290. g.setColor(0, 0, 0, 200 * tooltipFactor)
  291. g.rectangle('fill', x, y, width, height)
  292. g.setColor(255, 255, 255, 255 * tooltipFactor)
  293. g.printf(str, x + .01 * v, y + .01 * v, width)
  294. end
  295. end
  296. function MenuOptions:keyreleased(key)
  297. if key == 'escape' and self.active then
  298. self:toggle()
  299. return true
  300. end
  301. end
  302. function MenuOptions:mousepressed(mx, my, b)
  303. local u, v = ctx.u, ctx.v
  304. local x1 = u + self.offset
  305. local width = self.width * u
  306. if math.inside(mx, my, x1, 0, width, v) then
  307. if b == 'wd' then
  308. self:scrollPane(1)
  309. elseif b == 'wu' then
  310. self:scrollPane(-1)
  311. end
  312. return true
  313. end
  314. end
  315. function MenuOptions:gamepadpressed(gamepad, button)
  316. if button == 'b' and self.active then
  317. self:toggle()
  318. end
  319. end
  320. function MenuOptions:gamepadaxis(gamepad, axis, value)
  321. if axis == 'righty' then
  322. self:scrollPane(value)
  323. end
  324. end
  325. function MenuOptions:scrollPane(direction)
  326. local scrollSpeed = .05
  327. self.targetScroll = self.targetScroll + (ctx.v * scrollSpeed) * direction
  328. end
  329. function MenuOptions:resize()
  330. self.canvas = g.newCanvas((self.width + .05) * ctx.u, ctx.v)
  331. if self.active then self.offsetTween = tween.new(self.tweenDuration, self, {offset = -self.width * ctx.u}, 'inBack')
  332. else self.offsetTween = tween.new(self.tweenDuration, self, {offset = 0}, 'outBack') end
  333. table.clear(self.geometry)
  334. end
  335. function MenuOptions:toggle(force)
  336. if self.offsetTween.clock < self.tweenDuration then return end
  337. if self.active then
  338. self.offsetTween = tween.new(self.tweenDuration, self, {offset = 0}, 'inBack')
  339. else
  340. self.offsetTween = tween.new(self.tweenDuration, self, {offset = -self.width * ctx.u}, 'outBack')
  341. end
  342. self.active = not self.active
  343. end
  344. function MenuOptions:setMode(n)
  345. n = n or 0
  346. if n and n > 1 then return end
  347. if Context.started and not self.active then
  348. ctx:resize()
  349. return
  350. end
  351. local resolutions = love.window.getFullscreenModes()
  352. local dw, dh = love.window.getDesktopDimensions()
  353. local options = table.only(ctx.options, {'display', 'vsync', 'msaa'})
  354. MenuOptions.pixelScale = love.window and love.window.getPixelScale() or 1
  355. options.highdpi = true
  356. if not ctx.options.resolution then
  357. ctx.options.resolution = {resolutions[1].width, resolutions[1].height}
  358. end
  359. if tonumber(ctx.options.resolution[1]) == resolutions[1].width and tonumber(ctx.options.resolution[2]) == resolutions[1].height then
  360. options.fullscreen = true
  361. options.fullscreentype = 'desktop'
  362. else
  363. options.fullscreen = false
  364. end
  365. if love.window.setMode(ctx.options.resolution[1] / MenuOptions.pixelScale, ctx.options.resolution[2] / MenuOptions.pixelScale, options) then
  366. love.window.setTitle('Muju Juju')
  367. love.window.setIcon(love.image.newImageData('media/graphics/icon.png'))
  368. ctx:resize()
  369. if love.window.getPixelScale() == 2 then
  370. self:setMode(n + 1)
  371. end
  372. else
  373. print('There was a problem applying the requested window options... PANIC _>_>_>')
  374. end
  375. end