mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
819150f50f
* main menu scrollable when over 8 items * buggies * more buggies * Lorelei, Bruno, and Agatha now push their AfterBattle text right after a win * more and more bugs
215 lines
6.6 KiB
Lua
215 lines
6.6 KiB
Lua
-- Input abstraction: maps keyboard to Game Boy buttons.
|
|
-- `down` = held this frame; `pressed` = edge, consumed per fixed step.
|
|
|
|
local Input = {}
|
|
|
|
local DEFAULT_BINDINGS = {
|
|
up = "up", w = "up",
|
|
down = "down", s = "down",
|
|
left = "left", a = "left",
|
|
right = "right", d = "right",
|
|
z = "a", ["return"] = "a", space = "a",
|
|
x = "b", backspace = "b",
|
|
["kpenter"] = "start", escape = "start",
|
|
-- Select: fight-menu move reorder + bag item reorder. Tab is the
|
|
-- discoverable default (shown in CONTROLS); both shifts stay as
|
|
-- aliases so Right-Shift muscle memory from older builds still works.
|
|
tab = "select",
|
|
rshift = "select",
|
|
lshift = "select",
|
|
}
|
|
|
|
-- keys that map to "start" but also to "a" would conflict; keep Enter = a,
|
|
-- Escape = start for desktop friendliness.
|
|
|
|
-- LÖVE's standard gamepad mapping (SDL game controller DB), consistent
|
|
-- across Xbox/PlayStation/generic controllers on desktop and mobile. Some
|
|
-- third-party pads report their own SDL mapping for a given physical
|
|
-- button (e.g. Select/Back/View on off-brand XInput pads), which is what
|
|
-- src/ui/BindingsMenu.lua's rebinding is for -- see applyBindings below.
|
|
local DEFAULT_GAMEPAD_BINDINGS = {
|
|
dpup = "up", dpdown = "down", dpleft = "left", dpright = "right",
|
|
a = "a", b = "b",
|
|
start = "start", back = "select",
|
|
}
|
|
|
|
-- left-stick deadzones: press past STICK_ON, release once back under
|
|
-- STICK_OFF. The gap (hysteresis) stops the direction from flickering
|
|
-- while the stick sits near the threshold.
|
|
local STICK_ON = 0.5
|
|
local STICK_OFF = 0.3
|
|
|
|
function Input:init()
|
|
self:applyBindings(nil)
|
|
self:reset()
|
|
end
|
|
|
|
-- Layers a player's rebind choices (save.options.bindings, written by
|
|
-- src/ui/BindingsMenu.lua) on top of the defaults above. A rebind adds an
|
|
-- extra way to trigger that action instead of replacing the default key,
|
|
-- so e.g. Z/Enter/Space all still press A even after binding a 4th key to
|
|
-- it. Call whenever options load or change (see Game:applyOptions and
|
|
-- BindingsMenu:storeBinding) -- without this the menu records a choice
|
|
-- that never actually reaches gameplay.
|
|
function Input:applyBindings(overlay)
|
|
local keys, pads = {}, {}
|
|
for key, action in pairs(DEFAULT_BINDINGS) do keys[key] = action end
|
|
for button, action in pairs(DEFAULT_GAMEPAD_BINDINGS) do pads[button] = action end
|
|
for actionId, binding in pairs(overlay or {}) do
|
|
if type(binding) == "table" then
|
|
if binding.key then keys[binding.key] = actionId end
|
|
if binding.pad then pads[binding.pad] = actionId end
|
|
elseif type(binding) == "string" then
|
|
keys[binding] = actionId
|
|
end
|
|
end
|
|
self.keyBindings = keys
|
|
self.padBindings = pads
|
|
end
|
|
|
|
-- Purely event-driven state (press sets true, release sets false) has no
|
|
-- fallback if a release event never arrives -- focus loss, a minimized
|
|
-- window, or a disconnected gamepad can all swallow the key-up/button-up
|
|
-- that would have cleared a held direction. Called from Game on those
|
|
-- transitions so a stuck flag can't outlive them.
|
|
function Input:reset()
|
|
self.state = {}
|
|
self.pressQueue = {}
|
|
self.pressed = {}
|
|
self.sources = {}
|
|
self.stickAxis = { x = 0, y = 0 }
|
|
self.stickDir = nil
|
|
end
|
|
|
|
-- Multiple physical sources (W + Up, d-pad + stick, etc.) can claim the
|
|
-- same GB button. Track them individually so releasing one doesn't clear
|
|
-- a hold another source still owns, and so a press+release that both land
|
|
-- before the next FixedStep can't be revived when step() drains the queue.
|
|
local function press(self, btn, source)
|
|
local sources = self.sources[btn]
|
|
if not sources then
|
|
sources = {}
|
|
self.sources[btn] = sources
|
|
end
|
|
if not sources[source] then
|
|
sources[source] = true
|
|
table.insert(self.pressQueue, btn)
|
|
end
|
|
self.state[btn] = true
|
|
end
|
|
|
|
local function release(self, btn, source)
|
|
local sources = self.sources[btn]
|
|
if sources then
|
|
sources[source] = nil
|
|
if next(sources) == nil then
|
|
-- Leave an empty table (not nil) so step() can tell a real
|
|
-- source was released before the queue drained, versus a
|
|
-- synthetic pressQueue inject that never had sources at all.
|
|
self.state[btn] = false
|
|
end
|
|
else
|
|
self.state[btn] = false
|
|
end
|
|
end
|
|
|
|
function Input:keypressed(key)
|
|
local btn = self.keyBindings[key]
|
|
if btn then
|
|
press(self, btn, "key:" .. key)
|
|
end
|
|
end
|
|
|
|
function Input:keyreleased(key)
|
|
local btn = self.keyBindings[key]
|
|
if btn then
|
|
release(self, btn, "key:" .. key)
|
|
end
|
|
end
|
|
|
|
-- Called once per fixed step: promote queued presses to this step's edges.
|
|
-- Hold state is owned by live sources (updated in press/release), not
|
|
-- re-asserted here -- otherwise a same-frame press→release leaves the
|
|
-- button stuck on after the queue drains.
|
|
-- Synthetic injects (tests/drivers writing pressQueue directly, with no
|
|
-- source entry) still set state so scripted holds keep working.
|
|
function Input:step()
|
|
self.pressed = {}
|
|
for _, btn in ipairs(self.pressQueue) do
|
|
self.pressed[btn] = true
|
|
local sources = self.sources[btn]
|
|
if sources == nil then
|
|
-- synthetic pressQueue inject (tests/drivers): no live source map
|
|
self.state[btn] = true
|
|
elseif next(sources) ~= nil then
|
|
self.state[btn] = true
|
|
end
|
|
-- sources == {}: real press fully released before this step — keep up
|
|
end
|
|
for btn, sources in pairs(self.sources) do
|
|
if next(sources) == nil then
|
|
self.sources[btn] = nil
|
|
end
|
|
end
|
|
self.pressQueue = {}
|
|
end
|
|
|
|
function Input:gamepadpressed(joystick, button)
|
|
local btn = self.padBindings[button]
|
|
if btn then
|
|
press(self, btn, "pad:" .. button)
|
|
end
|
|
end
|
|
|
|
function Input:gamepadreleased(joystick, button)
|
|
local btn = self.padBindings[button]
|
|
if btn then
|
|
release(self, btn, "pad:" .. button)
|
|
end
|
|
end
|
|
|
|
-- left stick treated as a continuous held direction, same 4-way rule as
|
|
-- the touch swipe d-pad: whichever axis has the larger magnitude wins.
|
|
function Input:gamepadaxis(joystick, axis, value)
|
|
if axis == "leftx" then
|
|
self.stickAxis.x = value
|
|
elseif axis == "lefty" then
|
|
self.stickAxis.y = value
|
|
else
|
|
return
|
|
end
|
|
|
|
local x, y = self.stickAxis.x, self.stickAxis.y
|
|
local ax, ay = math.abs(x), math.abs(y)
|
|
local newDir = self.stickDir
|
|
if ax > STICK_ON or ay > STICK_ON then
|
|
if ax >= ay then
|
|
newDir = x > 0 and "right" or "left"
|
|
else
|
|
newDir = y > 0 and "down" or "up"
|
|
end
|
|
elseif ax < STICK_OFF and ay < STICK_OFF then
|
|
newDir = nil
|
|
end
|
|
|
|
if newDir ~= self.stickDir then
|
|
if self.stickDir then
|
|
release(self, self.stickDir, "stick")
|
|
end
|
|
if newDir then
|
|
press(self, newDir, "stick")
|
|
end
|
|
self.stickDir = newDir
|
|
end
|
|
end
|
|
|
|
function Input:isDown(btn)
|
|
return self.state[btn] or false
|
|
end
|
|
|
|
function Input:wasPressed(btn)
|
|
return self.pressed[btn] or false
|
|
end
|
|
|
|
return Input
|