local baton = { _VERSION = 'Baton v1.0.1', _DESCRIPTION = 'Input library for LÖVE.', _URL = 'https://github.com/tesselode/baton', _LICENSE = [[ MIT License Copyright (c) 2019 Andrew Minnich 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. ]] } -- string parsing functions -- -- splits a source definition into type and value -- example: 'button:a' -> 'button', 'a' local function parseSource(source) return source:match '(.+):(.+)' end -- splits an axis value into axis and direction -- example: 'leftx-' -> 'leftx', '-' local function parseAxis(value) return value:match '(.+)([%+%-])' end -- splits a joystick hat value into hat number and direction -- example: '2rd' -> '2', 'rd' local function parseHat(value) return value:match '(%d)(.+)' end --[[ -- source functions -- each source function checks the state of one type of input and returns a value from 0 to 1. for binary controls, such as keyboard keys and gamepad buttons, they return 1 if the input is held down and 0 if not. for analog controls, such as "leftx+" (the left analog stick held to the right), they return a number from 0 to 1. source functions are split into keyboard/mouse functions and joystick/gamepad functions. baton treats these two categories slightly differently. ]] local sourceFunction = {keyboardMouse = {}, joystick = {}} -- checks whether a keyboard key is down or not function sourceFunction.keyboardMouse.key(key) return love.keyboard.isDown(key) and 1 or 0 end -- checks whether a keyboard key is down or not, -- but it takes a scancode as an input function sourceFunction.keyboardMouse.sc(sc) return love.keyboard.isScancodeDown(sc) and 1 or 0 end -- checks whether a mouse buttons is down or not. -- note that baton doesn't detect mouse movement, just the buttons function sourceFunction.keyboardMouse.mouse(button) return love.mouse.isDown(tonumber(button)) and 1 or 0 end -- checks the position of a joystick axis function sourceFunction.joystick.axis(joystick, value) local axis, direction = parseAxis(value) -- "a and b or c" is ok here because b will never be boolean value = tonumber(axis) and joystick:getAxis(tonumber(axis)) or joystick:getGamepadAxis(axis) if direction == '-' then value = -value end return value > 0 and value or 0 end -- checks whether a joystick button is held down or not -- can take a number or a GamepadButton string function sourceFunction.joystick.button(joystick, button) -- i'm intentionally not using the "a and b or c" idiom here -- because joystick.isDown returns a boolean if tonumber(button) then return joystick:isDown(tonumber(button)) and 1 or 0 else return joystick:isGamepadDown(button) and 1 or 0 end end -- checks the direction of a joystick hat function sourceFunction.joystick.hat(joystick, value) local hat, direction = parseHat(value) return joystick:getHat(hat) == direction and 1 or 0 end --[[ -- player class -- the player object takes a configuration table and handles input accordingly. it's called a "player" because it makes sense to use multiple of these for each player in a multiplayer game, but you can use separate player objects to organize inputs however you want. ]] local Player = {} Player.__index = Player -- internal functions -- -- sets the player's config to a user-defined config table -- and sets some defaults if they're not already defined function Player:_loadConfig(config) if not config then error('No config table provided', 4) end if not config.controls then error('No controls specified', 4) end config.pairs = config.pairs or {} config.deadzone = config.deadzone or .5 config.squareDeadzone = config.squareDeadzone or false self.config = config end -- initializes a control object for each control defined in the config function Player:_initControls() self._controls = {} for controlName, sources in pairs(self.config.controls) do self._controls[controlName] = { sources = sources, rawValue = 0, value = 0, down = false, downPrevious = false, pressed = false, released = false, } end end -- initializes an axis pair object for each axis pair defined in the config function Player:_initPairs() self._pairs = {} for pairName, controls in pairs(self.config.pairs) do self._pairs[pairName] = { controls = controls, rawX = 0, rawY = 0, x = 0, y = 0, down = false, downPrevious = false, pressed = false, released = false, } end end function Player:_init(config) self:_loadConfig(config) self:_initControls() self:_initPairs() self._activeDevice = 'none' end --[[ detects the active device (keyboard/mouse or joystick). if the keyboard or mouse is currently being used, joystick inputs will be ignored. this is to prevent slight axis movements from adding errant inputs when someone's using the keyboard. the active device is saved to player._activeDevice, which is then used throughout the rest of the update loop to check only keyboard or joystick inputs. ]] function Player:_setActiveDevice() for _, control in pairs(self._controls) do for _, source in ipairs(control.sources) do local type, value = parseSource(source) if sourceFunction.keyboardMouse[type] then if sourceFunction.keyboardMouse[type](value) > self.config.deadzone then self._activeDevice = 'kbm' return end elseif self.config.joystick and sourceFunction.joystick[type] then if sourceFunction.joystick[type](self.config.joystick, value) > self.config.deadzone then self._activeDevice = 'joy' end end end end end --[[ gets the value of a control by running the appropriate source functions for all of its sources. does not apply deadzone. ]] function Player:_getControlRawValue(control) local rawValue = 0 for _, source in ipairs(control.sources) do local type, value = parseSource(source) if sourceFunction.keyboardMouse[type] and self._activeDevice == 'kbm' then if sourceFunction.keyboardMouse[type](value) == 1 then return 1 end elseif sourceFunction.joystick[type] and self._activeDevice == 'joy' then rawValue = rawValue + sourceFunction.joystick[type](self.config.joystick, value) if rawValue >= 1 then return 1 end end end return rawValue end --[[ updates each control in a player. saves the value with and without deadzone and the down/pressed/released state. ]] function Player:_updateControls() for _, control in pairs(self._controls) do control.rawValue = self:_getControlRawValue(control) control.value = control.rawValue >= self.config.deadzone and control.rawValue or 0 control.downPrevious = control.down control.down = control.value > 0 control.pressed = control.down and not control.downPrevious control.released = control.downPrevious and not control.down end end --[[ updates each axis pair in a player. saves the value with and without deadzone and the down/pressed/released state. ]] function Player:_updatePairs() for _, pair in pairs(self._pairs) do -- get raw x and y local l = self._controls[pair.controls[1]].rawValue local r = self._controls[pair.controls[2]].rawValue local u = self._controls[pair.controls[3]].rawValue local d = self._controls[pair.controls[4]].rawValue pair.rawX, pair.rawY = r - l, d - u -- limit to 1 local len = math.sqrt(pair.rawX^2 + pair.rawY^2) if len > 1 then pair.rawX, pair.rawY = pair.rawX / len, pair.rawY / len end -- deadzone if self.config.squareDeadzone then pair.x = math.abs(pair.rawX) > self.config.deadzone and pair.rawX or 0 pair.y = math.abs(pair.rawY) > self.config.deadzone and pair.rawY or 0 else pair.x = len > self.config.deadzone and pair.rawX or 0 pair.y = len > self.config.deadzone and pair.rawY or 0 end -- down/pressed/released pair.downPrevious = pair.down pair.down = pair.x ~= 0 or pair.y ~= 0 pair.pressed = pair.down and not pair.downPrevious pair.released = pair.downPrevious and not pair.down end end -- public API -- -- checks for changes in inputs function Player:update() self:_setActiveDevice() self:_updateControls() self:_updatePairs() end -- gets the value of a control or axis pair without deadzone applied function Player:getRaw(name) if self._pairs[name] then return self._pairs[name].rawX, self._pairs[name].rawY elseif self._controls[name] then return self._controls[name].rawValue else error('No control with name "' .. name .. '" defined', 3) end end -- gets the value of a control or axis pair with deadzone applied function Player:get(name) if self._pairs[name] then return self._pairs[name].x, self._pairs[name].y elseif self._controls[name] then return self._controls[name].value else error('No control with name "' .. name .. '" defined', 3) end end -- gets whether a control or axis pair is "held down" function Player:down(name) if self._pairs[name] then return self._pairs[name].down elseif self._controls[name] then return self._controls[name].down else error('No control with name "' .. name .. '" defined', 3) end end -- gets whether a control or axis pair was pressed this frame function Player:pressed(name) if self._pairs[name] then return self._pairs[name].pressed elseif self._controls[name] then return self._controls[name].pressed else error('No control with name "' .. name .. '" defined', 3) end end -- gets whether a control or axis pair was released this frame function Player:released(name) if self._pairs[name] then return self._pairs[name].released elseif self._controls[name] then return self._controls[name].released else error('No control with name "' .. name .. '" defined', 3) end end --[[ gets the currently active device (either "kbm", "joy", or "none"). this is useful for displaying instructional text. you may have a menu that says "press ENTER to confirm" or "press A to confirm" depending on whether the player is using their keyboard or gamepad. this function allows you to detect which they used most recently. ]] function Player:getActiveDevice() return self._activeDevice end -- main functions -- -- creates a new player with the user-provided config table function baton.new(config) local player = setmetatable({}, Player) player:_init(config) return player end return baton