diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1ae3ae0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Cambridge", + "type": "lua-local", + "request": "launch", + "program": { + "command": "lovec" + }, + "args": [ + "." + ], + "scriptRoots": [ + "." + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cad84f0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "Lua.workspace.library": [ + "${addons}/love2d/module/library" + ], + "Lua.runtime.version": "LuaJIT", + "Lua.runtime.special": { + "love.filesystem.load": "loadfile" + }, + "Lua.workspace.checkThirdParty": false +} \ No newline at end of file diff --git a/main.lua b/main.lua index 977593d..d82acc3 100644 --- a/main.lua +++ b/main.lua @@ -1,6 +1,12 @@ +if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE")=="1" then + LLDEBUGGER=require('lldebugger') + LLDEBUGGER.start() +end + function love.load() highscores = {} love.graphics.setDefaultFilter("linear", "nearest") + require "load.rpc" require "load.graphics" require "load.fonts" @@ -9,19 +15,69 @@ function love.load() require "load.save" require "load.bigint" require "load.version" - loadSave() + require "funcs" + TOUCH_SETTINGS = require 'mobile_libs.settings' + BUTTON = require 'mobile_libs.simple-button' + BUTTON.setDefaultOption{ + draw = function(self) + ---@type love.Font + self.font = self.font + + love.graphics.setColor(self.backgroundColor) + love.graphics.rectangle('fill', self.x, self.y, self.w, self.h, self.r) + + if self._pressed then + love.graphics.setColor(self.pressColor) + love.graphics.rectangle('fill', self.x, self.y, self.w, self.h, self.r) + elseif self._hovering then + love.graphics.setColor(self.hoverColor) + love.graphics.rectangle('fill', self.x, self.y, self.w, self.h, self.r) + end + + local text = type(self.text) == 'function' and self.text() or self.text + + local lineAmount + do + local _, t = self.font:getWrap(text, (self.w - 5) * 2) + lineAmount = #t + end + + local _font_height = self.font:getHeight() + + local textHeight = _font_height * (lineAmount * 0.5) + local textPos = self.y + (self.h * 0.5) - textHeight + + love.graphics.setColor(self.textColor) + love.graphics.setFont(self.font) + love.graphics.printf(text, self.x + 2.5, textPos, self.w - 5, self.textOrientation) + + love.graphics.setColor(self.borderColor) + love.graphics.setLineWidth(1) + love.graphics.rectangle('line', self.x, self.y, self.w, self.h, self.r) + end, + backgroundColor = {0, 0, 0, 0.8}, + pressColor = {0.4, 1, 1, 0.5}, + borderColor = {1, 1, 1, 0.8}, + font=font_3x5_2, + } + require 'mobile_libs.vctrl' + + loadSave() require "scene" - + --config["side_next"] = false --config["reverse_rotate"] = true --config["das_last_key"] = false --config["fullscreen"] = false love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true}); - + -- used for screenshots GLOBAL_CANVAS = love.graphics.newCanvas() + -- Used for transforming 2D positions + GLOBAL_TRANSFORM = love.math.newTransform() + love.resize(love.graphics.getWidth(), love.graphics.getHeight()) -- aliasing to prevent people using math.random by accident math.random = love.math.random @@ -78,7 +134,7 @@ function love.draw() (height - scale_factor * 480) / 2 ) love.graphics.scale(scale_factor) - + scene:render() if config.gamesettings.display_gamemode == 1 or scene.title == "Title" then @@ -89,9 +145,9 @@ function love.draw() "fps - " .. version, 0, 460, 635, "right" ) end - + love.graphics.pop() - + love.graphics.setCanvas() love.graphics.setColor(1,1,1,1) love.graphics.draw(GLOBAL_CANVAS) @@ -114,6 +170,9 @@ function love.keypressed(key, scancode) scene.restart_message = true if config.secret then playSE("mode_decide") else playSE("erase", "single") end + --TEST + elseif scancode == "f9" and scene.title == "Title" then + scene = TouchConfigScene() -- f12 is reserved for saving screenshots elseif scancode == "f12" then local ss_name = os.date("ss/%Y-%m-%d_%H-%M-%S.png") @@ -126,7 +185,7 @@ function love.keypressed(key, scancode) GLOBAL_CANVAS:newImageData():encode("png", ss_name) -- function keys are reserved elseif string.match(scancode, "^f[1-9]$") or string.match(scancode, "^f[1-9][0-9]+$") then - return + return -- escape is reserved for menu_back elseif scancode == "escape" then scene:onInputPress({input="menu_back", type="key", key=key, scancode=scancode}) @@ -146,7 +205,7 @@ function love.keyreleased(key, scancode) scene:onInputRelease({input="menu_back", type="key", key=key, scancode=scancode}) -- function keys are reserved elseif string.match(scancode, "^f[1-9]$") or string.match(scancode, "^f[1-9][0-9]+$") then - return + return -- handle all other keys; tab is reserved, but the input config scene keeps it from getting configured as a game input, so pass tab to the scene here else local input_released = nil @@ -192,7 +251,7 @@ function love.joystickaxis(joystick, axis, value) config.input.joysticks and config.input.joysticks[joystick:getName()] and config.input.joysticks[joystick:getName()].axes and - config.input.joysticks[joystick:getName()].axes[axis] + config.input.joysticks[joystick:getName()].axes[axis] then if math.abs(value) >= 1 then input_pressed = config.input.joysticks[joystick:getName()].axes[axis][value >= 1 and "positive" or "negative"] @@ -283,6 +342,14 @@ end function love.resize(w, h) GLOBAL_CANVAS:release() GLOBAL_CANVAS = love.graphics.newCanvas(w, h) + + SCREEN_SCALE_FACTOR = math.min(w / 640, h / 480) + GLOBAL_TRANSFORM:setTransformation( + (w - SCREEN_SCALE_FACTOR * 640) / 2, + (h - SCREEN_SCALE_FACTOR * 480) / 2, + 0, + SCREEN_SCALE_FACTOR + ) end -- higher values of TARGET_FPS will make the game run "faster" @@ -316,7 +383,7 @@ function love.run() if love.timer then processBGMFadeout(love.timer.step()) end - + if scene and scene.update and love.timer then scene:update() diff --git a/mobile_libs/file.lua b/mobile_libs/file.lua new file mode 100644 index 0000000..44619b3 --- /dev/null +++ b/mobile_libs/file.lua @@ -0,0 +1,41 @@ +local FILE = {} +local binser = require "libs.binser" +-- local bitser = require "libs.bitser" + +local serializer_used + +function FILE.serialize(data) + if serializer_used == 'bitser' then + return bitser.dumps(data) + else + return binser.serialize(data) + end +end + +function FILE.deserialize(data) + if serializer_used == 'bitser' then + return bitser.loads(data) + else + return binser.deserialize(data)[1] + end +end + +function FILE.read(path) + if love.filesystem.getInfo(path) then + return FILE.deserialize(love.filesystem.read(path)) + else + error("No file: "..path) + end +end + +function FILE.write(path, data) + love.filesystem.write(path, FILE.serialize(data)) +end + +---@param lib_name 'bitser'|'binser' +---Init the FILE module with chosen serializer +return function(lib_name) + assert(lib_name == 'bitser' or lib_name == 'binser', '[lib_name] must be "bitser" or "binser"') + serializer_used = lib_name + _G.FILE = FILE +end \ No newline at end of file diff --git a/mobile_libs/settings.lua b/mobile_libs/settings.lua new file mode 100644 index 0000000..b6186c1 --- /dev/null +++ b/mobile_libs/settings.lua @@ -0,0 +1,38 @@ +local fs = love.filesystem + +local CONFIG_FILE = '/mobile/touch_config.txt' +local _settings = fs.read(CONFIG_FILE) ~= nil and FILE.read(CONFIG_FILE) or {} +local _defaultSettings = { + firstTime = true, + __default__={ + {type='button',x= 70,y=280,key= 'up',r=45,iconSize=60,alpha=0.4}, + {type='button',x= 70,y=430,key= 'down',r=45,iconSize=60,alpha=0.4}, + {type='button',x= -5,y=355,key= 'left',r=45,iconSize=60,alpha=0.4}, + {type='button',x= 145,y=355,key= 'right',r=45,iconSize=60,alpha=0.4}, + {type='button',x=640- -5,y=355,key= 'rotate_left',r=45,iconSize=60,alpha=0.4}, + {type='button',x=640-145,y=355,key= 'rotate_left2',r=45,iconSize=60,alpha=0.4}, + {type='button',x=640- 70,y=430,key= 'rotate_right',r=45,iconSize=60,alpha=0.4}, + {type='button',x=640- 70,y=280,key='rotate_right2',r=45,iconSize=60,alpha=0.4}, + {type='button',x=320, y=420,key= 'restart',r=35,iconSize=60,alpha=0.4}, + }, + + ---@type table[] + bind = {}, +} + +return setmetatable( + {__default__ = _defaultSettings}, + { + __index = function(_, k) + if _settings[k] == nil then + _settings[k] = _defaultSettings[k] + FILE.write(CONFIG_FILE,_settings) + end + return _settings[k] + end, + __newindex = function(_, k, v) + _settings[k] = v + FILE.write(CONFIG_FILE,_settings) + end + } +) \ No newline at end of file diff --git a/mobile_libs/simple-button.lua b/mobile_libs/simple-button.lua new file mode 100644 index 0000000..5ebd4bc --- /dev/null +++ b/mobile_libs/simple-button.lua @@ -0,0 +1,323 @@ +-- SIMPLE-BUTTON.lua
+-- A simple module that aims to help you quickly create buttons
+-- It is can be used as a base class to help you quickly creating button
+-- This module has type notations so IntelliSense should give you some suggestions
+local BUTTON = {} + +-- MIT License + +-- Copyright (c) 2024 SweetSea-ButImNotSweet + +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: + +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. + +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +local NULL = function() end +local function checkColorTableValidation(C) + if C and type(C) == "table" and (#C == 3 or #C == 4) then + for _, v in pairs(C) do + if type(v) ~= "number" or v<0 or v>1 then return false end + end + else + return false + end + return true +end + +---@class BUTTON.button +---@field text? string|function # Name of the button, will be used to show +---@field textOrientation? "center"|"justify"|"left"|"right" +--- +---@field x? number # Position of the button (x, y, w, h) +---@field y? number # Position of the button (x, y, w, h) +---@field w? number # Position of the button (x, y, w, h) +---@field h? number # Position of the button (x, y, w, h) +---@field r? number # Radius corner, cannot larger than half of button's width and half of button's height +--- +---@field borderWidth? number|1 # Line width will be used to draw button +---@field borderJoin? "bevel"|"miter"|"none" +---@field borderStyle? "rough"|"smooth" +--- +---@field font? love.Font +--- +---@field backgroundColor? integer[] +---@field textColor? integer[] +---@field borderColor? integer[] +---@field hoverColor? integer[] +---@field pressColor? integer[] +--- +---@field codeWhenPressed? function| # Code will be execute when pressed +---@field codeWhenReleased? function| # Code will be execute when released +---@field drawingButtonFunc? function| # The function is used to draw text
You can override the default one if you feel the default text drawing function is not suitable for you +--- +---@field draw? function +---@field update? function +local button = { + textOrientation = "center", + r = 0, + + backgroundColor = {0,0,0,0}, + hoverColor = {1,1,1,0.5}, + pressColor = {0, 1, 0.5, 0.5}, + borderColor = {1,1,1}, + textColor = {1,1,1}, + + font = love.graphics.newFont(15), + + borderWidth = 1, + borderJoin = "none", + borderStyle = "smooth", + + codeWhenPressed = NULL, + codeWhenReleased = NULL, + update = NULL, + + _hovering = false, + _pressed = false, + _touchID = false, +}; button.__index = button +function button:draw() + love.graphics.setLineWidth(self.borderWidth) + love.graphics.setLineStyle(self.borderStyle) + love.graphics.setLineJoin(self.borderJoin) + + love.graphics.setColor(self.backgroundColor) + love.graphics.rectangle('fill', self.x, self.y, self.w, self.h, self.r) + + if self._pressed then + love.graphics.setColor(self.pressColor) + love.graphics.rectangle('fill', self.x, self.y, self.w, self.h, self.r) + elseif self._hovering then + love.graphics.setColor(self.hoverColor) + love.graphics.rectangle('fill', self.x, self.y, self.w, self.h, self.r) + end + + love.graphics.setColor(self.textColor) + love.graphics.setFont(self.font) + + local text = type(self.text) == 'function' and self.text() or self.text + + local lineAmount + do + local _, t = self.font:getWrap(text, self.w) + lineAmount = #t + end + local textHeight = self.font:getHeight() * (lineAmount * 0.5) + local textPos = self.y + (self.h * 0.5) - textHeight + love.graphics.printf(text, self.x, textPos, self.w, self.textOrientation) + + love.graphics.setColor(self.borderColor) + love.graphics.rectangle('line', self.x, self.y, self.w, self.h, self.r) +end +---Check if current position is hovering the button, if it is, return true +function button:isHovering(x,y) + if not y then return false end + if + x >= self.x and + y >= self.y and + x <= self.x + self.w and + y <= self.y + self.h + then + return true + else + return false + end +end +---Trigger press action, only when ``self._hovering`` is true +function button:press(x, y, touchID) + if self:isHovering(x, y) and not self._pressed then + self._touchID = touchID + self._pressed = true + + self.codeWhenPressed() + self:draw() + + return true + end +end +---Trigger release action, don't need ``self._hovering`` to ``true`` +function button:release(x, y, touchID) + local valid + if touchID then + valid = touchID == self._touchID + else + valid = true + end + + if valid then + self._pressed = false + self._touchID = false + + if touchID then + self._hovering = false + else + self._hovering = self:isHovering(x, y) + end + + if self:isHovering(x, y) then + self.codeWhenReleased() + return true + end + end +end + +---@param D BUTTON.button|BUTTON.newData +---@param safe? boolean @ Creating widget? If not then ignore accept missing important parameters +---@return nil +---Validate the provided data, will be called by ``BUTTON.new`` and ``BUTTON.setDefaultOption``
+---***WARNING! THIS FUNCTION WILL RAISE EXCEPTION IF DATA IS INVALID!*** +function BUTTON.checkDataValidation(D, safe) + if not safe then + if type(D.text) == 'function' then + assert(type(D.text()) == 'string', "[text] is a function but it doesn't return any string?!") + elseif type(D.text) ~= 'string' then + error("[text] must be a string or a function returns string, got "..type(D.text)) + end + + assert(type(D.x) == "number" , "[x] must be a integer") + assert(type(D.y) == "number" , "[y] must be a integer") + assert(type(D.w) == "number" and D.w > 0, "[w] must be a positive integer") + assert(type(D.h) == "number" and D.h > 0, "[h] must be a positive integer") + assert((type(D.r) == "number" and D.r >= 0 and D.r <= D.w * 0.5 and D.r <= D.h * 0.5) or D.r == nil, "[r] must be a positive integer and cannot larger than half of button's width and half of button's height") + else + assert(type(D.r) == "number" and D.r >= 0 or D.r == nil, "[r] must be a positive integer (CAUTION: a extra condition is temproraily ignored because you are setting default option)") + end + assert(table.contains({"center","justify","left","right"}, D.textOrientation) or D.textOrientation == nil, "[borderJoin] must be 'bevel', 'miter' or 'none") + assert((type(D.borderWidth) == "number" and D.borderWidth > 0) or D.borderWidth == nil, "[borderWidth] must be a postive integer") + assert(table.contains({"bevel", "miter", "none"}, D.borderJoin) or D.borderJoin == nil, "[borderJoin] must be 'bevel', 'miter' or 'none") + assert(table.contains({"rough", "smooth"}, D.borderStyle) or D.borderStyle == nil, "[borderStyle] must be 'rough' or 'smooth'") + assert((D.font and D.font.typeOf and D.font:typeOf("Font")) or D.font == nil, "[font] must be love.Font") + + assert(checkColorTableValidation(D.backgroundColor), "[backgroundColor] must be a table with r, g, b (, a) values, all of them must be integers between 0 and 1") + assert(checkColorTableValidation(D.hoverColor), "[hoverColor] must be a table with r, g, b (, a) values, all of them must be integers between 0 and 1") + assert(checkColorTableValidation(D.pressColor), "[hoverColor] must be a table with r, g, b (, a) values, all of them must be integers between 0 and 1") + assert(checkColorTableValidation(D.borderColor), "[borderColor] must be a table with r, g, b (, a) values, all of them must be integers between 0 and 1") + assert(checkColorTableValidation(D.textColor), "[textColor] must be a table with r, g, b (, a) values, all of them must be integers between 0 and 1") + + assert(type(D.codeWhenPressed) == "function" or D.codeWhenPressed == nil, "[codeWhenPressed] must be a function or nil") + assert(type(D.codeWhenReleased) == "function" or D.codeWhenReleased == nil, "[codeWhenReleased] must be a function or nil") + assert(type(D.drawingButtonFunc) == "function" or D.drawingButtonFunc == nil, "[drawingButtonFunc] must be a function or nil") +end + +---@class BUTTON.newData +---@field text string|function # Name of the button, will be used to show. If function provided, it should return the string! +---@field textOrientation? "center"|"justify"|"left"|"right" +--- +---@field x number # Position of the button (x, y, w, h) +---@field y number # Position of the button (x, y, w, h) +---@field w number # Position of the button (x, y, w, h) +---@field h number # Position of the button (x, y, w, h) +---@field r? number # Radius corner, cannot larger than half of button's width and half of button's height +--- +---@field borderWidth? number|1 # Line width will be used to draw button +---@field borderJoin? "bevel"|"miter"|"none" +---@field borderStyle? "rough"|"smooth" +--- +---@field font? love.Font +--- +---@field backgroundColor? integer[] +---@field textColor? integer[] +---@field borderColor? integer[] +---@field hoverColor? integer[] +---@field pressColor? integer[] +--- +---@field codeWhenPressed? function| # Code will be execute when pressed +---@field codeWhenReleased? function| # Code will be execute when released +---@field drawingButtonFunc? function| # The function is used to draw text
You can override the default one if you feel the default text drawing function is not suitable for you +--- +---@field draw? function +---@field update? function + +---@param D BUTTON.newData +---@nodiscard +---Create a new button, provide you a table with 4 functions inside: draw and update, press and release
+---You need to put them into intended callbacks :) +--- +---Remember to fill 5 necessary parameters: name, x, y, w and h +function BUTTON.new(D) + local B = setmetatable(D, button) + BUTTON.checkDataValidation(B) + return B +end + +---@param D BUTTON.button +function BUTTON.setDefaultOption(D) + BUTTON.checkDataValidation(setmetatable(D, button), true) + for k, v in pairs(D) do + if button[k] ~= nil then + button[k] = v + else + error("Parameter named ["..k.."] is not existed or cannot be set default value, in BUTTON!") + end + end +end + +-- < EXTRA GENERAL OPTIONS > + +---Draw all buttons in provided list +---@param list table +function BUTTON.draw(list) + for _, v in pairs(list) do v:draw() end +end +---Update all buttons in provided list +---@param list table +function BUTTON.update(list) + for _, v in pairs(list) do v:update() end +end + +---Check if current mouse position is inside button
+---Calling BUTTON.press will trigger this, but you can call it when moving mouse so button can be highlighted when being hovered +---@param list table +---@param x number # Mouse position +---@param y number # Mouse position +function BUTTON.checkHovering(list, x, y) + local highlighted_a_button = false + for _, v in pairs(list) do + if highlighted_a_button then + v._hovering = false + else + v._hovering = v:isHovering(x, y) + end + + if not highlighted_a_button and v._hovering then highlighted_a_button = true end + end +end + +--- Trigger the press action, only if ``button._hovering == true`` +---@param list table +---@param x number # Mouse position +---@param y number # Mouse position +function BUTTON.press(list, x, y, touchID) + for _, v in pairs(list) do if v:press(x, y, touchID) then return true end end +end + +---Trigger the release action +---@param list table +function BUTTON.release(list, x, y, touchID) + for _, v in pairs(list) do if v:release(x, y, touchID) then return true end end +end + +--- Do a reset, useful for switching scenes +function BUTTON.reset(list) + for _, v in pairs(list) do + v._pressed = false + v._hovering = false + v._touchID = false + end +end + +return BUTTON \ No newline at end of file diff --git a/mobile_libs/vctrl.lua b/mobile_libs/vctrl.lua new file mode 100644 index 0000000..ad601af --- /dev/null +++ b/mobile_libs/vctrl.lua @@ -0,0 +1,245 @@ +local gc_newQuad=love.graphics.newQuad + +---Get distance between two points +---@param x1 number +---@param y1 number +---@param x2 number +---@param y2 number +---@return number +local function math_distance(x1,y1,x2,y2) + return ((x1-x2)^2+(y1-y2)^2)^.5 +end + +local function mDrawQ(obj,quad,x,y,a,k) + local _,_,w,h=quad:getViewport() + love.graphics.draw(obj,quad,x,y,a,k,nil,w*.5,h*.5) +end + +local empty_quad +-- A table containing quads used to draw icons for virtual control system. +-- local virtual_quad=setmetatable((function() +-- local t={} +-- local w=180 +-- empty_quad=gc_newQuad(0,0,1,1,5*w,7*w) +-- for i,name in next,{ +-- 'left','right','up','down','', +-- 'rotate_right','rotate_left','','','', +-- '','','','','', +-- '','','','','', +-- '','','menu_back','','', +-- '','','','','', +-- '','','','','menu_decide', +-- } do if #name>0 then t[name]=gc_newQuad((i-1)%5*w,math.floor((i-1)/5)*w,w,w,5*w,7*w) end end +-- t.rotate_right2, t.rotate_left2 = t.rotate_right, t.rotate_left +-- return t +-- end)(),{ +-- __index=function() return empty_quad end +-- }) +local virtual_quad=setmetatable((function() + local t={} + local w=180 + empty_quad=gc_newQuad(0,0,1,1,5*w,2*w) + for i,name in next,{ + 'left','right','up','down','restart', + 'rotate_right','rotate_left','rotate_right2','rotate_left2' + } do if #name>0 then t[name]=gc_newQuad((i-1)%5*w,math.floor((i-1)/5)*w,w,w,5*w,2*w) end end + return t +end)(),{ + __index=function() return empty_quad end +}) +local virtual_texture=love.graphics.newImage('mobile_libs/vctrlTexture.png') + + +local control_type={} + +control_type.button={} +control_type.button.__index=control_type.button +function control_type.button:new(data) + local data=data or {} + return setmetatable({ + show=data.show==nil and true or data.show, + x=data.x or 320, + y=data.y or 240, + r=data.r or 80, -- size + shape=data.shape or 'circle', + key=data.key or 'X', + iconSize=data.iconSize or 60, + alpha=data.alpha or 0.75, + quad=virtual_quad[data.key] + },self) +end +function control_type.button:export() + return { + type = 'button', + show = self.show, + x = self.x, + y = self.y, + r = self.r, + shape = self.shape, + key = self.key, + iconSize = self.iconSize, + alpha = self.alpha + } +end +function control_type.button:reset() + self.pressed=false + self.lastPressTime=-1e99 + self.pressingID=false +end +function control_type.button:press(_,_,id) + self.pressed=true + self.lastPressTime=love.timer.getTime() + self.pressingID=id + -- love.keypressed(self.key, love.keyboard.getScancodeFromKey(self.key)) + SCENE:onInputPress{input=self.key,type="virtual"} +end +function control_type.button:release() + self.pressed=false + self.pressingID=false + -- love.keyreleased(self.key,love.keyboard.getScancodeFromKey(self.key)) + SCENE:onInputRelease{input=self.key,type="virtual"} +end +function control_type.button:drag(dx,dy) + self.x,self.y=self.x+dx,self.y+dy +end +function control_type.button:draw(forceAlpha) + local alpha = forceAlpha or self.alpha + love.graphics.setLineWidth(4) + if self.shape=='circle' then + love.graphics.setColor(0,0,0,alpha) + love.graphics.circle('fill',self.x,self.y,self.r-4) + + love.graphics.setColor(1,1,1,self.pressed and .5 or 0) + love.graphics.circle('fill',self.x,self.y,self.r-4) + + love.graphics.setColor(1,1,1,alpha) + love.graphics.circle('line',self.x,self.y,self.r-2) + elseif self.shape=='square' then + love.graphics.setColor(0,0,0,alpha) + love.graphics.rectangle('fill',self.x-self.r-4,self.y-self.r-4,self.r*2+8,self.r*2+8) + + love.graphics.setColor(1,1,1,self.pressed and .5 or 0) + love.graphics.rectangle('fill',self.x-self.r-4,self.y-self.r-4,self.r*2+8,self.r*2+8) + + love.graphics.setColor(1,1,1,alpha) + love.graphics.rectangle('line',self.x-self.r-2,self.y-self.r-2,self.r*2+4,self.r*2+4) + end + if self.iconSize>0 and self.quad then + love.graphics.setColor(1,1,1,alpha) + local _,_,w,h=self.quad:getViewport() + mDrawQ( + virtual_texture, + self.quad, + self.x,self.y,0, + self.iconSize/100*math.min(self.r*2/w,self.r*2/h) + ) + end +end +function control_type.button:getDistance(x,y) + if self.shape=='circle' then + return math_distance(x,y,self.x,self.y)/self.r + elseif self.shape=='square' then + return math.max(math.abs(x-self.x),math.abs(y-self.y))/self.r + end +end + +local touches={} +local global_toggle=false +VCTRL={} +VCTRL.focus=nil -- Focusing buttons +VCTRL.hasChanged = false + +---@class VCTRL.data +---@field type 'button' +---@field x number +---@field y number +---@field shape? string +---@field key? string +---@field iconSize? number +---@field alpha? number +---@field show? boolean + +---@param ... VCTRL.data[] +---Adding (multiple) virtual button(s) +function VCTRL.new(...) + for _,d in pairs(...) do table.insert(VCTRL,control_type[d.type]:new(d)) end +end + +---@param toggle boolean|false +---Enabling virtual control or not +function VCTRL.toggle(toggle) + if not toggle then + -- Release all buttons to prevent button ghost situation + for id, b in pairs(touches) do + b:release(id) + touches[id]=nil + end + end + global_toggle=toggle +end + +function VCTRL.clearAll() + local toggle = global_toggle + VCTRL.toggle(false) + global_toggle = toggle + + for i=#VCTRL,1,-1 do VCTRL[i] = nil end + collectgarbage() +end + +---@param force? boolean Forcing click on hidden widgets? +function VCTRL.press(x,y,id,force) + if not (global_toggle and id) then return end + local obj,closestDist=false,1e99 + for _, w in ipairs(VCTRL) do + if w.show or force then + local d=w:getDistance(x,y) + if d<=1 and dSHOW<\nhide" or "show\n>HIDE<" + else + return "show\nhide" + end + end, + x = 400, y = 110, w = 60, h = 40, + codeWhenReleased = function() + if focusingButton then + focusingButton.show = not focusingButton.show + VCTRL.hasChanged = true + end + end, + update = function(self) self.textColor = focusingButton and {1, 1, 1} or {0.5, 0.5, 0.5} end + }, + -- previewToggle = BUTTON.new{ + -- text = "Preview\nON", + -- x = 570, y = 60, w = 60, h = 40, + -- codeWhenReleased = function() + -- VCTRL.release() + -- BUTTON.release(buttonList) + -- SCENE = TouchConfigPreviewScene() + -- end + -- }, + resetAll = BUTTON.new{ + text = "RESET\nALL", + x = 500, y = 110, w = 60, h = 40, + codeWhenReleased = function() + local selection = love.window.showMessageBox( + "Save config?", "Are you really sure about RESETTING ALL touchscreen configuration?", + {"Yes", "No", escapebutton = 2, enterbutton = 1}, + "info", true + ) + if selection == 1 then + VCTRL.focus = nil; focusingButton = nil + VCTRL.hasChanged = false + VCTRL.clearAll() + VCTRL.new(TOUCH_SETTINGS.__default__) + TOUCH_SETTINGS.bind[1] = TOUCH_SETTINGS.__default__ + end + end + }, + menuScreen = BUTTON.new{ + text = "MENU", + x = 570, y = 10, w = 60, h = 40, + codeWhenReleased = function() + if VCTRL.hasChanged or TOUCH_SETTINGS.firstTime then + local selection = love.window.showMessageBox( + "Save config?", "Do you want to save your changes before exiting?", + {"Save", "Discard", "Keep editing", escapebutton = 3, enterbutton = 1}, + "info", true + ) + if selection == 1 then + TOUCH_SETTINGS.bind[1] = VCTRL.exportAll() + -- love.window.showMessageBox("Saved!", "Your changes was saved!") + + exitSceneFunc(true) + elseif selection == 2 then + VCTRL.clearAll() + VCTRL.new(TOUCH_SETTINGS.bind[1]) + -- love.window.showMessageBox("Discarded!", "Your changes was discarded!") + + exitSceneFunc() + end + else + exitSceneFunc() + end + end + } +} +sliderList.buttonSize = newSlider( + 200, 30, 120, 0, 0, 120, + function(v) + if focusingButton then + v = roundUnit(v, 5) + if focusingButton.r ~= v then + focusingButton.r = v + VCTRL.hasChanged = true + end + sliderList.buttonSize.value = v / 120 + end + end, + {width = 40} +) +sliderList.iconSize = newSlider( + 480, 30, 120, 0, 0, 100, + function(v) + if focusingButton then + v = roundUnit(v, 5) + if focusingButton.iconSize ~= v then + focusingButton.iconSize = v + VCTRL.hasChanged = true + end + sliderList.iconSize.value = v / 100 + end + end, + {width = 40} +) +sliderList.opacity = newSlider( + 200, 80, 120, 0, 0, 1, + function() + local v + if focusingButton then + v = roundUnit(sliderList.opacity.value, 0.01) + if focusingButton.alpha~=v then + focusingButton.alpha = v + VCTRL.hasChanged = true + end + sliderList.opacity.value = v + end + end, + {width = 40} +) +local gridSizeTable = {1, 2, 5, 10, 20, 50, 100} +sliderList.gridSize = newSlider( + 480, 80, 120, 1, 1, #gridSizeTable - 1, + function() + local f = #gridSizeTable - 1 + local v = roundUnit(sliderList.gridSize.value, 1 / f) + sliderList.gridSize.value = v + snapUnit = gridSizeTable[roundUnit(v * f + 1)] + end, + {width = 40} +); sliderList.gridSize.forceLight = true + +local function sliderList_draw() + for _, s in pairs(sliderList) do + if s.forceLight then + love.graphics.setColor(1, 1, 1) + else + love.graphics.setColor(focusingButton and {1, 1, 1} or {0.5, 0.5, 0.5}) + end + love.graphics.setLineWidth(1) + s:draw() + end +end + +local function sliderList_update() + local x, y + if #love.touch.getTouches() == 1 then + x, y = GLOBAL_TRANSFORM:inverseTransformPoint(love.touch.getPosition(love.touch.getTouches()[1])) + else + x, y = GLOBAL_TRANSFORM:inverseTransformPoint(love.mouse.getPosition()) + end + for _, s in pairs(sliderList) do + s:update(x, y, #love.touch.getTouches() == 1 or love.mouse.isDown(1)) + end +end + +function TouchConfigScene:new() + VCTRL.toggle(true) + + VCTRL.focus = nil + focusingButton = nil +end +function TouchConfigScene:update() + -- TODO + if VCTRL.focus~=focusingButton then + focusingButton = VCTRL.focus + sliderList.opacity.value = focusingButton.alpha + sliderList.buttonSize.value = focusingButton.r / 120 + sliderList.iconSize.value = focusingButton.iconSize / 100 + end + + BUTTON.update(buttonList) + sliderList_update() +end + +local string_format = string.format +function TouchConfigScene:render() + drawBackground("title_night") + + if snapUnit >= 5 then + local x1, y1 = GLOBAL_TRANSFORM:inverseTransformPoint(0, 0) + local x2, y2 = GLOBAL_TRANSFORM:inverseTransformPoint(love.graphics.getDimensions()) + + love.graphics.setColor(1,1,1,math.sin(love.timer.getTime()*4)*.1+.25) + love.graphics.setLineWidth(1) + -- From 0 to X + for i=x1, x2+snapUnit, snapUnit do + local x = i - i % snapUnit + love.graphics.line(x, y1, x, y2) + end + -- From 0 to Y + for i=y1,y2+snapUnit,snapUnit do + local y= i - i % snapUnit + love.graphics.line(x1, y, x2, y) + end + end + + love.graphics.setColor(0, 0, 0, 0.7) + love.graphics.rectangle("fill", 5, 5, 560, 100) + + -- Button Size + drawText(string_format("Size (buttons)\n%14.1d", focusingButton and focusingButton.r or 0), 10, 13, 100, "left") + -- Icon size + drawText(string_format("Size (icons)\n%13.1d%%", focusingButton and focusingButton.iconSize or 0), 290, 13, 100, "left") + -- Opacity + drawText(string_format("Opacity\n%13.1d%%", focusingButton and focusingButton.alpha * 100 or 0), 10, 63, 100, "left") + -- Snap to grid + drawText(string_format("Snap to grid\n%14.1d", snapUnit), 290, 63, 100, "left") + + for _, v in ipairs(VCTRL) do + if v ~= focusingButton then + v:draw( + focusingButton and + (v.show and 0.5 or 0.1) or + (v.show and 1 or 0.5) + ) + end + end + if focusingButton then + focusingButton:draw( + math.clamp( + math.sin(love.timer.getTime()*4)*.5+0.1, + focusingButton.show and 1 or 0.1, 1 + ) + ) + end + + sliderList_draw() + BUTTON.draw(buttonList) +end + +---@param e SCENE_onInput +function TouchConfigScene:onInputMove(e) + if e.type == "touch" or (e.type == "mouse" and love.mouse.isDown(1)) then + if VCTRL.drag(e.dx, e.dy, e.id or 1) then VCTRL.hasChanged = true end + elseif e.type == "mouse" then + BUTTON.checkHovering(buttonList, e.x, e.y) + end +end +---@param e SCENE_onInput +function TouchConfigScene:onInputPress(e) + if e.type == "mouse" or e.type == "touch" then + if not ( + VCTRL.press(e.x, e.y, e.id and e.id or 1, true) or + BUTTON.press(buttonList, e.x, e.y, e.id) or + (e.x >= 120 and e.x <= 280 and e.y >= 10 and e.y <= 100) or + (e.x >= 400 and e.x <= 560 and e.y >= 10 and e.y <= 100) + ) then + VCTRL.focus = nil + focusingButton = nil + end + end +end +---@param e SCENE_onInput +function TouchConfigScene:onInputRelease(e) + if e.type == "mouse" or e.type == "touch" then + if not BUTTON.release(buttonList, e.x, e.y, e.id) then + if focusingButton and VCTRL.release(e.id or 1) then + focusingButton.x = roundUnit(focusingButton.x, snapUnit) + focusingButton.y = roundUnit(focusingButton.y, snapUnit) + end + end + end +end + +return TouchConfigScene \ No newline at end of file diff --git a/scene.lua b/scene.lua index cc5a2f6..b5347ef 100644 --- a/scene.lua +++ b/scene.lua @@ -21,3 +21,4 @@ TuningScene = require "scene.tuning" SettingsScene = require "scene.settings" CreditsScene = require "scene.credits" TitleScene = require "scene.title" +TouchConfigScene = require "mobile_scene.touch_config" \ No newline at end of file