1453 lines
62 KiB
Lua
1453 lines
62 KiB
Lua
LOVELY_INTEGRITY = 'd36e3953c2739cd4ea15794fb5e4031a2dac09fc33baadd0a2820e4726891304'
|
|
|
|
---@class Controller
|
|
Controller = Object:extend()
|
|
|
|
--The controller contains all engine logic for how human input interacts with any game objects.
|
|
function Controller:init()
|
|
|
|
--Each of these are calculated per frame to pass along to the corresponding nodes for input handling
|
|
self.clicked = {target = nil, handled = true, prev_target = nil} --The node that was clicked this frame
|
|
self.focused = {target = nil, handled = true, prev_target = nil } --The node that is being focused on this frame, only applies when using controller
|
|
self.dragging = {target = nil, handled = true, prev_target = nil} --The node being dragged this frame
|
|
self.hovering = {target = nil, handled = true, prev_target = nil} --The node being hovered this frame
|
|
self.released_on = {target = nil, handled = true, prev_target = nil} --The node that the cursor 'Released' on, like letting go of LMB
|
|
|
|
self.collision_list = {} --A list of all node that the cursor currently collides with
|
|
|
|
--Input values to be determined by this controller - the actual game objects should not have to see any of this
|
|
self.cursor_down = {T = {x=0, y=0}, target = nil, time = 0, handled = true}
|
|
self.cursor_up = {T = {x=0, y=0}, target = nil, time = 0.1, handled = true}
|
|
self.cursor_hover = {T = {x=0, y=0}, target = nil, time = 0, handled = true}
|
|
self.cursor_collider = nil --The node that collides with the cursor this frame
|
|
self.cursor_position = {x=0,y=0} --NOT IN GAME UNITS
|
|
|
|
--For key presses, hold times, and if they are released directly from LOVE
|
|
self.pressed_keys = {}
|
|
self.held_keys = {}
|
|
self.held_key_times = {}
|
|
self.released_keys = {}
|
|
|
|
--For button presses, hold times, and if they are released directly from LOVE
|
|
self.pressed_buttons = {}
|
|
self.held_buttons = {}
|
|
self.held_button_times = {}
|
|
self.released_buttons = {}
|
|
|
|
--For all controller interrupts
|
|
self.interrupt = {
|
|
focus = false,
|
|
}
|
|
|
|
--For all controller locks
|
|
self.locks = {}
|
|
self.locked = nil
|
|
|
|
--Buttons pressed and released during axis updates
|
|
self.axis_buttons = {
|
|
l_stick = {current = '', previous = ''},
|
|
r_stick = {current = '', previous = ''},
|
|
l_trig = {current = '', previous = ''},
|
|
r_trig = {current = '', previous = ''}
|
|
}
|
|
|
|
--The speed that the controller thumbstick moves the cursor
|
|
self.axis_cursor_speed = 20
|
|
|
|
--A registry of buttons, each a valid button input name corresponding to a node (likely a button). This is modified with the registry functions
|
|
self.button_registry = {}
|
|
|
|
--A node representing where the cursor should 'snap' to. When this is set, then next frame should have the cursor to that position or on that node. Use :snap_to
|
|
self.snap_cursor_to = nil
|
|
|
|
--A stack of cursor positions, this stack changes depending on the depth of menus on screen so the game can remember where you last had your cursor
|
|
--This needs to keep track of both positions and nodes if possible, as well as the depth
|
|
self.cursor_context = {
|
|
layer = 1,
|
|
stack = {}
|
|
}
|
|
|
|
self.cardarea_context = {}
|
|
|
|
--Human Interface device flags, these are set per frame to ensure that correct controller updates are taking place
|
|
self.HID = {
|
|
last_type = '',
|
|
dpad = false,
|
|
pointer = true,
|
|
touch = false,
|
|
controller = false,
|
|
mouse = true,
|
|
axis_cursor = false,
|
|
}
|
|
|
|
--The gamepad most recently used, if any
|
|
self.GAMEPAD = {object = nil, mapping = nil, name = nil}
|
|
self.GAMEPAD_CONSOLE = '' --Valid button icons for Xbox, Playstation and Nintendo controllers
|
|
|
|
--If we need an emulated gamepad for keyboard controls
|
|
self.keyboard_controller = {
|
|
getGamepadMappingString = function() return 'balatro_kbm' end,
|
|
getGamepadAxis = function() return 0 end
|
|
}
|
|
|
|
self.is_cursor_down = false
|
|
end
|
|
|
|
--Sets the gamepad to be the updated gamepad, searches for the console type and sets the art button pips accordingly
|
|
--Some code here is from github.com/idbrii/love-gamepadguesser (MIT License)
|
|
function Controller:set_gamepad(_gamepad)
|
|
if self.GAMEPAD.object ~= _gamepad then
|
|
self.GAMEPAD.object = _gamepad
|
|
self.GAMEPAD.mapping = _gamepad:getGamepadMappingString() or ''
|
|
self.GAMEPAD.name = self.GAMEPAD.mapping:match("^%x*,(.-),") or ''
|
|
self.GAMEPAD.temp_console = self:get_console_from_gamepad(self.GAMEPAD.name)
|
|
if self.GAMEPAD_CONSOLE ~= self.GAMEPAD.temp_console then
|
|
self.GAMEPAD_CONSOLE = self.GAMEPAD.temp_console
|
|
for k, v in pairs(G.I.SPRITE) do
|
|
if v.atlas == G.ASSET_ATLAS["gamepad_ui"] then
|
|
v.sprite_pos.y = G.CONTROLLER.GAMEPAD_CONSOLE == 'Nintendo' and 2 or G.CONTROLLER.GAMEPAD_CONSOLE == 'Playstation' and (G.F_PS4_PLAYSTATION_GLYPHS and 3 or 1) or 0
|
|
v:set_sprite_pos(v.sprite_pos)
|
|
end
|
|
end
|
|
end
|
|
self.GAMEPAD.temp_console = nil
|
|
end
|
|
end
|
|
|
|
--Some code here is from github.com/idbrii/love-gamepadguesser (MIT License)
|
|
function Controller:get_console_from_gamepad(_gamepad)
|
|
G.ARGS.gamepad_patterns = G.ARGS.gamepad_patterns or
|
|
{
|
|
Playstation = {"%f[%w]PS%d%f[%D]", "Sony%f[%W]", "Play[Ss]tation"},
|
|
Nintendo = {"Wii%f[%L]", "%f[%u]S?NES%f[%U]", "%f[%l]s?nes%f[%L]", "%f[%u]Switch%f[%L]", "Joy[- ]Cons?%f[%L]",},
|
|
--Keyboard = {'balatro_kbm'}
|
|
}
|
|
|
|
for k, v in pairs(G.ARGS.gamepad_patterns) do
|
|
for kk, vv in ipairs(v) do
|
|
if _gamepad:match(vv) then
|
|
return k
|
|
end
|
|
end
|
|
end
|
|
return 'Xbox'
|
|
end
|
|
|
|
--The universal controller for what type of HID Device the player is using to interact with the game. The Game should be able to handle switching
|
|
--to any viable HID at any time
|
|
function Controller:set_HID_flags(HID_type, button)
|
|
--we need to determine it the axis input will be handled like a button or pointer
|
|
--if button and self.HID.controller and not string.find(button, 'dp') then return end
|
|
if HID_type == 'axis' then
|
|
self.HID.controller = true
|
|
self.HID.last_type = 'axis'
|
|
elseif HID_type and HID_type ~= self.HID.last_type then
|
|
self.HID.dpad = HID_type == 'button'
|
|
self.HID.pointer = HID_type == 'mouse' or HID_type == 'axis_cursor' or HID_type == 'touch'
|
|
self.HID.controller = HID_type == 'button' or HID_type == 'axis_cursor'
|
|
self.HID.mouse = HID_type == 'mouse'
|
|
self.HID.touch = HID_type == 'touch'
|
|
self.HID.axis_cursor = HID_type == 'axis_cursor'
|
|
self.HID.last_type = HID_type
|
|
|
|
if self.HID.mouse then
|
|
love.mouse.setVisible(true)
|
|
else
|
|
love.mouse.setVisible(false)
|
|
end
|
|
end
|
|
if not self.HID.controller then
|
|
self.GAMEPAD_CONSOLE = ''
|
|
self.GAMEPAD.object = nil
|
|
self.GAMEPAD.mapping = nil
|
|
self.GAMEPAD.name = nil
|
|
end
|
|
end
|
|
|
|
--Sets the current position of the cursor
|
|
function Controller:set_cursor_position()
|
|
--If using a mouse for the cursor
|
|
if self.HID.mouse or self.HID.touch then
|
|
self.interrupt.focus = false
|
|
--Never focus if using the mouse
|
|
if self.focused.target then
|
|
self.focused.target.states.focus.is = false
|
|
self.focused.target= nil
|
|
end
|
|
|
|
--Set the position of the cursor to the love position of the mouse, derive cursor transform from that
|
|
self.cursor_position.x, self.cursor_position.y = love.mouse.getPosition()
|
|
G.CURSOR.T.x = self.cursor_position.x/(G.TILESCALE*G.TILESIZE)
|
|
G.CURSOR.T.y = self.cursor_position.y/(G.TILESCALE*G.TILESIZE)
|
|
G.CURSOR.VT.x = G.CURSOR.T.x
|
|
G.CURSOR.VT.y = G.CURSOR.T.y
|
|
end
|
|
end
|
|
|
|
--Called every game logic update frame
|
|
function Controller:update(dt)
|
|
|
|
--parse all locks and set
|
|
self.locked = false
|
|
if G.screenwipe then self.locks.wipe = true else self.locks.wipe = false end
|
|
|
|
for k, v in pairs(self.locks) do
|
|
if v then self.locked = true end
|
|
end
|
|
|
|
if self.locks.frame_set then
|
|
self.locks.frame_set = nil
|
|
self.overlay_timer = 0
|
|
G.E_MANAGER:add_event(Event({
|
|
trigger = 'after',
|
|
delay = 0.1,
|
|
timer = 'UPTIME',
|
|
blocking = false,
|
|
blockable = false,
|
|
no_delete = true,
|
|
func = (function()
|
|
self.locks.frame = nil
|
|
return true
|
|
end)
|
|
}))
|
|
end
|
|
|
|
self.overlay_timer = self.overlay_timer or 0
|
|
if G.OVERLAY_MENU then
|
|
self.overlay_timer = self.overlay_timer + dt
|
|
else
|
|
self.overlay_timer = 0
|
|
end
|
|
|
|
if self.overlay_timer > 1.5 then self.locks.frame = nil end
|
|
|
|
--Remove anything from the registry that is no longer in game
|
|
self:cull_registry()
|
|
|
|
--Calculate the axis update and set the HID flags if there is any axis input
|
|
self:set_HID_flags(self:update_axis(dt))
|
|
|
|
--Set the cursor to be visible only if we are using a mouse or an axis to control the cursor position
|
|
if self.HID.pointer and not (self.HID.mouse or self.HID.touch) and not self.interrupt.focus then
|
|
G.CURSOR.states.visible = true
|
|
else
|
|
G.CURSOR.states.visible = false
|
|
end
|
|
|
|
--For mouse input, reset any controller things and set the cursor to be where the mouse is
|
|
self:set_cursor_position()
|
|
|
|
--Handle all the button updates and key updates, call the required functions
|
|
if not G.screenwipe then
|
|
--Key press and release handling
|
|
for k, v in pairs(self.pressed_keys) do
|
|
if v then self:key_press_update(k, dt) end
|
|
end
|
|
for k, v in pairs(self.held_keys) do
|
|
if v then self:key_hold_update(k, dt) end
|
|
end
|
|
for k, v in pairs(self.released_keys) do
|
|
if v then self:key_release_update(k, dt) end
|
|
end
|
|
|
|
--Button press and release handling
|
|
for k, v in pairs(self.pressed_buttons) do
|
|
if v then self:button_press_update(k, dt) end
|
|
end
|
|
for k, v in pairs(self.held_buttons) do
|
|
if v then self:button_hold_update(k, dt) end
|
|
end
|
|
for k, v in pairs(self.released_buttons) do
|
|
if v then self:button_release_update(k, dt) end
|
|
end
|
|
end
|
|
|
|
self.frame_buttonpress = false
|
|
|
|
--reset press and release lists
|
|
self.pressed_keys = EMPTY(self.pressed_keys)
|
|
self.released_keys = EMPTY(self.released_keys)
|
|
self.pressed_buttons= EMPTY(self.pressed_buttons)
|
|
self.released_buttons = EMPTY(self.released_buttons)
|
|
|
|
|
|
--If using controller, update context and snap tos
|
|
if self.HID.controller then
|
|
--If there is a node/position to snap to from the cursor context layer
|
|
if self.cursor_context.stack[self.cursor_context.layer] then
|
|
local _context = self.cursor_context.stack[self.cursor_context.layer]
|
|
self:snap_to{node = (_context.node and not _context.node.REMOVED and _context.node), T = _context.cursor_pos}
|
|
self.interrupt.stack = _context.interrupt
|
|
self.cursor_context.stack[self.cursor_context.layer] = nil
|
|
end
|
|
--If there is a card the was being dragged but no longer is, snap to it
|
|
if self.dragging.prev_target and not self.dragging.target and getmetatable(self.dragging.prev_target) == Card and not self.dragging.prev_target.REMOVED then
|
|
--Overly complicated coyote time focus, so the user can quickly select cards without things going wonky
|
|
if not self.COYOTE_FOCUS then
|
|
self:snap_to{node = self.dragging.prev_target}
|
|
else
|
|
self.COYOTE_FOCUS = nil
|
|
end
|
|
end
|
|
--If the cursor should snap to a location
|
|
if self.snap_cursor_to then
|
|
self.interrupt.focus = self.interrupt.stack
|
|
self.interrupt.stack = false
|
|
if self.snap_cursor_to.type == 'node' and not self.snap_cursor_to.node.REMOVED then
|
|
self.focused.prev_target = self.focused.target
|
|
self.focused.target = self.snap_cursor_to.node
|
|
self:update_cursor()
|
|
elseif self.snap_cursor_to.type == 'transform' then
|
|
self:update_cursor(self.snap_cursor_to.T)
|
|
end
|
|
if self.focused.prev_target ~= self.focused.target and self.focused.prev_target then self.focused.prev_target.states.focus.is = false end
|
|
self.snap_cursor_to = nil
|
|
end
|
|
end
|
|
|
|
--Reset all collision states, get every node that collides with the cursor, then update the focus and hover targets
|
|
self:get_cursor_collision(G.CURSOR.T)
|
|
self:update_focus()
|
|
self:set_cursor_hover()
|
|
if self.L_cursor_queue then
|
|
self:L_cursor_press(self.L_cursor_queue.x, self.L_cursor_queue.y)
|
|
self.L_cursor_queue = nil
|
|
end
|
|
|
|
self.dragging.prev_target = self.dragging.target
|
|
self.released_on.prev_target = self.released_on.target
|
|
self.clicked.prev_target = self.clicked.target
|
|
self.hovering.prev_target = self.hovering.target
|
|
|
|
--Cursor is currently down
|
|
if not self.cursor_down.handled then
|
|
if self.cursor_down.target.states.drag.can then
|
|
self.cursor_down.target.states.drag.is = true
|
|
self.cursor_down.target:set_offset(self.cursor_down.T, 'Click')
|
|
self.dragging.target = self.cursor_down.target
|
|
self.dragging.handled = false
|
|
end
|
|
self.cursor_down.handled = true
|
|
end
|
|
|
|
if not self.cursor_up.handled then
|
|
--First, stop dragging
|
|
if self.dragging.target then
|
|
self.dragging.target:stop_drag()
|
|
self.dragging.target.states.drag.is = false
|
|
self.dragging.target = nil
|
|
end
|
|
--Now, handle the Cursor release
|
|
--Was the Cursor release in the same location as the Cursor press and within Cursor timeout?
|
|
if self.cursor_down.target then
|
|
if (not self.cursor_down.target.click_timeout or self.cursor_down.target.click_timeout*G.SPEEDFACTOR > self.cursor_up.time - self.cursor_down.time) then
|
|
if Vector_Dist(self.cursor_down.T, self.cursor_up.T) < G.MIN_CLICK_DIST then
|
|
if self.cursor_down.target.states.click.can then
|
|
self.clicked.target = self.cursor_down.target
|
|
self.clicked.handled = false
|
|
end
|
|
--if not, was the Cursor dragging some other thing?
|
|
elseif self.dragging.prev_target and self.cursor_up.target and self.cursor_up.target.states.release_on.can then
|
|
self.released_on.target = self.cursor_up.target
|
|
self.released_on.handled = false
|
|
end
|
|
end
|
|
end
|
|
self.cursor_up.handled = true
|
|
end
|
|
|
|
--Cursor is currently hovering over something
|
|
if self.cursor_hover.target and self.cursor_hover.target.states.hover.can and (not self.HID.touch or self.is_cursor_down) then
|
|
self.hovering.target = self.cursor_hover.target
|
|
if self.hovering.prev_target and self.hovering.prev_target ~= self.hovering.target then self.hovering.prev_target.states.hover.is = false end
|
|
self.hovering.target.states.hover.is = true
|
|
self.hovering.target:set_offset(self.cursor_hover.T, 'Hover')
|
|
elseif (self.cursor_hover.target == nil or (self.HID.touch and not self.is_cursor_down)) and self.hovering.target then
|
|
self.hovering.target.states.hover.is = false
|
|
self.hovering.target = nil
|
|
end
|
|
|
|
--------------------------------------------------------------------
|
|
-- Sending all input updates to the game objects
|
|
--------------------------------------------------------------------
|
|
--The clicked object
|
|
if not self.clicked.handled then
|
|
self.clicked.target:click()
|
|
self.clicked.handled = true
|
|
end
|
|
|
|
--Process registry clicks
|
|
self:process_registry()
|
|
|
|
--The object being dragged
|
|
if self.dragging.target then
|
|
self.dragging.target:drag()
|
|
end
|
|
|
|
--The object released on
|
|
if not self.released_on.handled and self.dragging.prev_target then
|
|
if self.dragging.prev_target == self.hovering.target then self.hovering.target:stop_hover();self.hovering.target = nil end
|
|
self.released_on.target:release(self.dragging.prev_target)
|
|
self.released_on.handled = true
|
|
end
|
|
|
|
--The object being hovered over
|
|
if self.hovering.target then
|
|
self.hovering.target:set_offset(self.cursor_hover.T, 'Hover')
|
|
if self.hovering.prev_target ~= self.hovering.target then
|
|
if self.hovering.target ~= self.dragging.target and not self.HID.touch then
|
|
self.hovering.target:hover()
|
|
elseif self.HID.touch then
|
|
local _ID = self.hovering.target.ID
|
|
G.E_MANAGER:add_event(Event({
|
|
trigger = 'after',
|
|
blockable = false,
|
|
blocking = false,
|
|
delay = G.MIN_HOVER_TIME,
|
|
func = function()
|
|
if self.hovering.target and _ID == self.hovering.target.ID then
|
|
self.hovering.target:hover()
|
|
end
|
|
return true
|
|
end
|
|
}))
|
|
if self.hovering.prev_target then
|
|
self.hovering.prev_target:stop_hover()
|
|
end
|
|
end
|
|
if self.hovering.prev_target then
|
|
self.hovering.prev_target:stop_hover()
|
|
end
|
|
end
|
|
elseif self.hovering.prev_target then
|
|
self.hovering.prev_target:stop_hover()
|
|
end
|
|
if self.hovering.target and self.hovering.target == self.dragging.target and not self.HID.touch then
|
|
self.hovering.target:stop_hover()
|
|
end
|
|
end
|
|
|
|
--Brute force remove all registries that no longer have valid nodes
|
|
function Controller:cull_registry()
|
|
for k, registry in pairs(self.button_registry) do
|
|
for i=#registry, 1, -1 do
|
|
if registry[i].node.REMOVED then
|
|
table.remove(registry, i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--Adds a node to the controller registry. Supply the button that will be pressed in order to click this node
|
|
--
|
|
---@param node Node The node that will be clicked when the registry is pressed
|
|
---@param registry string The button to register, must be a valid gamepad input
|
|
function Controller:add_to_registry(node, registry)
|
|
--If the button doesn't have a registry list yet, add it
|
|
self.button_registry[registry] = self.button_registry[registry] or {}
|
|
|
|
--There really should only ever be one entry per registered button, but that is hard sometimes with all the stuff on screen.
|
|
--If that does happen, the most recently registered one will be used and the old one will be kept in case we remove the new button and want to keep the old binding
|
|
table.insert(self.button_registry[registry], 1, {node = node, menu = (not not G.OVERLAY_MENU) or (not not G.SETTINGS.paused)})
|
|
end
|
|
|
|
--Process any click function of any nodes that have been clicked in the button registry
|
|
function Controller:process_registry()
|
|
for _, registry in pairs(self.button_registry) do
|
|
for i=1, #registry do
|
|
if registry[i].click and registry[i].node.click then
|
|
if registry[i].menu == not not G.OVERLAY_MENU and
|
|
registry[i].node.T.x > -2 and registry[i].node.T.x < G.ROOM.T.w + 2 and
|
|
registry[i].node.T.y > -2 and registry[i].node.T.y < G.ROOM.T.h + 2 then
|
|
registry[i].node:click()
|
|
end
|
|
registry[i].click = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--Add or remove layers from the context for the cursor. This allows the cursor to 'snap' back to the previous layer when the current layer is removed\
|
|
--in such cases where a menu on screen is removed or nested menus are being navigated
|
|
--
|
|
---@param delta number The direction to modify the cursor context, 1 to add a layer, -1 to remove a layer, -1000 to remove all layers except for the base
|
|
function Controller:mod_cursor_context_layer(delta)
|
|
--Add a layer to the context, reference the node but if that node has been removed also save the cursor position too
|
|
if delta == 1 then
|
|
local prev_cursor_context = {node = self.focused.target, cursor_pos = {x = G.CURSOR.T.x, y = G.CURSOR.T.y}, interrupt = self.interrupt.focus}
|
|
self.cursor_context.stack[self.cursor_context.layer] = prev_cursor_context
|
|
self.cursor_context.layer = self.cursor_context.layer + 1
|
|
|
|
--remove the top layer from the stack
|
|
elseif delta == -1 then
|
|
self.cursor_context.stack[self.cursor_context.layer] = nil
|
|
self.cursor_context.layer = self.cursor_context.layer - 1
|
|
|
|
--remove all but the base layer from the stack
|
|
elseif delta == -1000 then
|
|
self.cursor_context.layer = 1
|
|
self.cursor_context.stack = {self.cursor_context.stack[1]}
|
|
|
|
--remove all layers
|
|
elseif delta == -2000 then
|
|
self.cursor_context.layer = 1
|
|
self.cursor_context.stack = {}
|
|
end
|
|
|
|
--Navigate focus, will default to the top layer on the stack
|
|
self:navigate_focus()
|
|
end
|
|
|
|
--Snap the cursor to a particular node or transform
|
|
function Controller:snap_to(args)
|
|
self.snap_cursor_to = {node = args.node, T = args.T, type = args.node and 'node' or 'transform'}
|
|
end
|
|
|
|
--saves the focus context to be loaded in the future, for example if the shop is rerolled while a card is highlighted
|
|
function Controller:save_cardarea_focus(_cardarea)
|
|
if G[_cardarea] then
|
|
if self.focused.target and self.focused.target.area and self.focused.target.area == G[_cardarea] then
|
|
self.cardarea_context[_cardarea] = self.focused.target.rank
|
|
return true
|
|
else
|
|
self.cardarea_context[_cardarea] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
--recalls the focus context for a particular cardarea
|
|
function Controller:recall_cardarea_focus(_cardarea)
|
|
local ca_string = nil
|
|
if type(_cardarea) == 'string' then ca_string = _cardarea; _cardarea = G[_cardarea] end
|
|
|
|
if _cardarea and (not self.focused.target or
|
|
self.interrupt.focus or
|
|
(not self.interrupt.focus and self.focused.target.area and self.focused.target.area == _cardarea)) then
|
|
if ca_string and self.cardarea_context[ca_string] then
|
|
for i = self.cardarea_context[ca_string], 1, -1 do
|
|
if _cardarea.cards[i] then
|
|
self:snap_to({node = _cardarea.cards[i]})
|
|
self.interrupt.focus = false
|
|
break
|
|
end
|
|
end
|
|
elseif _cardarea.cards and _cardarea.cards[1] then
|
|
self:snap_to({node = _cardarea.cards[1]})
|
|
self.interrupt.focus = false
|
|
end
|
|
end
|
|
if ca_string then self.cardarea_context[ca_string] = nil end
|
|
end
|
|
|
|
--Updated the location of the cursor, either with a specific T or if there is a Node target
|
|
function Controller:update_cursor(hard_set_T)
|
|
if hard_set_T then
|
|
G.CURSOR.T.x = hard_set_T.x
|
|
G.CURSOR.T.y = hard_set_T.y
|
|
self.cursor_position.x = G.CURSOR.T.x*(G.TILESCALE*G.TILESIZE)
|
|
self.cursor_position.y = G.CURSOR.T.y*(G.TILESCALE*G.TILESIZE)
|
|
G.CURSOR.VT.x = G.CURSOR.T.x
|
|
G.CURSOR.VT.y = G.CURSOR.T.y
|
|
return
|
|
end
|
|
if self.focused.target then
|
|
self.cursor_position.x, self.cursor_position.y = self.focused.target:put_focused_cursor()
|
|
G.CURSOR.T.x = self.cursor_position.x/(G.TILESCALE*G.TILESIZE)
|
|
G.CURSOR.T.y = self.cursor_position.y/(G.TILESCALE*G.TILESIZE)
|
|
G.CURSOR.VT.x = G.CURSOR.T.x
|
|
G.CURSOR.VT.y = G.CURSOR.T.y
|
|
end
|
|
end
|
|
|
|
--Helper function to set the button presses/releases for the values determined in update_axis()
|
|
function Controller:handle_axis_buttons()
|
|
for _, v in pairs(G.CONTROLLER.axis_buttons) do
|
|
--Button is no longer being pressed
|
|
if v.previous ~= '' and (v.current == '' or v.previous ~= v.current) then
|
|
G.CONTROLLER:button_release(v.previous)
|
|
end
|
|
--New button is being pressed
|
|
if v.current ~= '' and v.previous ~= v.current then
|
|
G.CONTROLLER:button_press(v.current)
|
|
end
|
|
end
|
|
end
|
|
|
|
--Handles all axis input for left stick, right stick and triggers. Treats them as buttons or cursors depending on context
|
|
function Controller:update_axis(dt)
|
|
--Keep track of if there is any cursor movement from the axis changes
|
|
local axis_interpretation = nil
|
|
|
|
--Advance all the axis buttons to determine if there were any changes
|
|
self.axis_buttons.l_stick.previous = self.axis_buttons.l_stick.current; self.axis_buttons.l_stick.current = ''
|
|
self.axis_buttons.r_stick.previous = self.axis_buttons.r_stick.current; self.axis_buttons.r_stick.current = ''
|
|
self.axis_buttons.l_trig.previous = self.axis_buttons.l_trig.current; self.axis_buttons.l_trig.current = ''
|
|
self.axis_buttons.r_trig.previous = self.axis_buttons.r_trig.current; self.axis_buttons.r_trig.current = ''
|
|
|
|
if self.HID.controller then
|
|
---------------------------------------------------------------
|
|
-- Left Thumbstick
|
|
---------------------------------------------------------------
|
|
local l_stick_x = self.GAMEPAD.object:getGamepadAxis('leftx')
|
|
local l_stick_y = self.GAMEPAD.object:getGamepadAxis('lefty')
|
|
--If there is something being dragged, we want to treat the left stick as a cursor input
|
|
if self.dragging.target and math.abs(l_stick_x) + math.abs(l_stick_y) > 0.1 then
|
|
axis_interpretation = 'axis_cursor' --There is some cursor movement
|
|
|
|
--deadzone of 10% for each axis of l_stick
|
|
if math.abs(l_stick_x) < 0.1 then l_stick_x = 0 end
|
|
if math.abs(l_stick_y) < 0.1 then l_stick_y = 0 end
|
|
l_stick_x = l_stick_x + (l_stick_x>0 and -0.1 or 0) + (l_stick_x<0 and 0.1 or 0)
|
|
l_stick_y = l_stick_y + (l_stick_y>0 and -0.1 or 0) + (l_stick_y<0 and 0.1 or 0)
|
|
|
|
--Modify the cursor position according to the l_stick axis values
|
|
G.CURSOR.T.x = G.CURSOR.T.x + dt*l_stick_x*self.axis_cursor_speed
|
|
G.CURSOR.T.y = G.CURSOR.T.y + dt*l_stick_y*self.axis_cursor_speed
|
|
G.CURSOR.VT.x = G.CURSOR.T.x
|
|
G.CURSOR.VT.y = G.CURSOR.T.y
|
|
|
|
self.cursor_position.x = G.CURSOR.T.x*(G.TILESCALE*G.TILESIZE)
|
|
self.cursor_position.y = G.CURSOR.T.y*(G.TILESCALE*G.TILESIZE)
|
|
|
|
--If nothing is being dragged, we want to treat the left stick as a dpad input
|
|
else
|
|
self.axis_buttons.l_stick.current = self.axis_buttons.l_stick.previous
|
|
if math.abs(l_stick_x) + math.abs(l_stick_y) > 0.5 then
|
|
axis_interpretation = 'button' --left stick is no longer a cursor, can be set below from the right stick though
|
|
|
|
self.axis_buttons.l_stick.current = math.abs(l_stick_x) > math.abs(l_stick_y) and
|
|
(l_stick_x > 0 and 'dpright' or 'dpleft') or
|
|
(l_stick_y > 0 and 'dpdown' or 'dpup')
|
|
elseif math.abs(l_stick_x) + math.abs(l_stick_y) < 0.3 then
|
|
self.axis_buttons.l_stick.current = ''
|
|
end
|
|
end
|
|
|
|
---------------------------------------------------------------
|
|
-- Right Thumbstick
|
|
---------------------------------------------------------------
|
|
local r_stick_x = self.GAMEPAD.object:getGamepadAxis('rightx')
|
|
local r_stick_y = self.GAMEPAD.object:getGamepadAxis('righty')
|
|
G.DEADZONE = 0.2
|
|
local mag = math.sqrt(math.abs(r_stick_x)^2 + math.abs(r_stick_y)^2)
|
|
if mag > G.DEADZONE then
|
|
axis_interpretation = 'axis_cursor' --There is some cursor movement
|
|
|
|
--deadzone of 20% for each axis of l_stick
|
|
if math.abs(r_stick_x) < G.DEADZONE then r_stick_x = 0 end
|
|
if math.abs(r_stick_y) < G.DEADZONE then r_stick_y = 0 end
|
|
r_stick_x = r_stick_x + (r_stick_x>0 and -G.DEADZONE or 0) + (r_stick_x<0 and G.DEADZONE or 0)
|
|
r_stick_y = r_stick_y + (r_stick_y>0 and -G.DEADZONE or 0) + (r_stick_y<0 and G.DEADZONE or 0)
|
|
|
|
--Modify the cursor position according to the l_stick axis values
|
|
G.CURSOR.T.x = G.CURSOR.T.x + dt*r_stick_x*self.axis_cursor_speed
|
|
G.CURSOR.T.y = G.CURSOR.T.y + dt*r_stick_y*self.axis_cursor_speed
|
|
G.CURSOR.VT.x = G.CURSOR.T.x
|
|
G.CURSOR.VT.y = G.CURSOR.T.y
|
|
|
|
self.cursor_position.x = G.CURSOR.T.x*(G.TILESCALE*G.TILESIZE)
|
|
self.cursor_position.y = G.CURSOR.T.y*(G.TILESCALE*G.TILESIZE)
|
|
end
|
|
|
|
---------------------------------------------------------------
|
|
-- Triggers
|
|
---------------------------------------------------------------
|
|
--Triggers are just buttons
|
|
local l_trig = self.GAMEPAD.object:getGamepadAxis('triggerleft')
|
|
local r_trig = self.GAMEPAD.object:getGamepadAxis('triggerright')
|
|
|
|
--Set the current to be the same as previous, only set to '' if the trigger value is low enough
|
|
self.axis_buttons.l_trig.current = self.axis_buttons.l_trig.previous
|
|
self.axis_buttons.r_trig.current = self.axis_buttons.r_trig.previous
|
|
|
|
--Make a tilting effect when you press the triggers while hovering over a node that has tilt_var
|
|
if self.focused.target and self.focused.target.tilt_var then
|
|
--self.focused.target.tilt_var.dx = 0.1*(r_trig - l_trig) + 0.9*self.focused.target.tilt_var.dx
|
|
end
|
|
|
|
if l_trig > 0.5 then
|
|
self.axis_buttons.l_trig.current = 'triggerleft'
|
|
elseif l_trig < 0.3 then
|
|
self.axis_buttons.l_trig.current = ''
|
|
end
|
|
if r_trig > 0.5 then
|
|
self.axis_buttons.r_trig.current = 'triggerright'
|
|
elseif r_trig < 0.3 then
|
|
self.axis_buttons.r_trig.current = ''
|
|
end
|
|
|
|
--return 'button' if trigger is being used and axis is not
|
|
if self.axis_buttons.r_trig.current ~= '' or self.axis_buttons.l_trig.current ~= '' then
|
|
axis_interpretation = axis_interpretation or 'button'
|
|
end
|
|
|
|
self:handle_axis_buttons()
|
|
end
|
|
|
|
if axis_interpretation then self.interrupt.focus = false end
|
|
|
|
return axis_interpretation
|
|
end
|
|
|
|
function Controller:button_press_update(button, dt)
|
|
if self.locks.frame then return end
|
|
self.held_button_times[button] = 0
|
|
self.interrupt.focus = false
|
|
|
|
if not self:capture_focused_input(button, 'press', dt) then
|
|
if button == "dpup" then
|
|
self:navigate_focus('U')
|
|
end
|
|
if button == "dpdown" then
|
|
self:navigate_focus('D')
|
|
end
|
|
if button == "dpleft" then
|
|
self:navigate_focus('L')
|
|
end
|
|
if button == "dpright" then
|
|
self:navigate_focus('R')
|
|
end
|
|
end
|
|
|
|
if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
|
|
self.frame_buttonpress = true
|
|
|
|
if self.button_registry[button] and self.button_registry[button][1] and not self.button_registry[button][1].node.under_overlay then
|
|
self.button_registry[button][1].click = true
|
|
else
|
|
if button == 'start' then
|
|
if G.STATE == G.STATES.SPLASH then
|
|
G:delete_run()
|
|
G:main_menu()
|
|
end
|
|
end
|
|
if button == "a" then
|
|
if self.focused.target and
|
|
self.focused.target.config.focus_args and
|
|
self.focused.target.config.focus_args.type == 'slider' and
|
|
(not G.CONTROLLER.HID.mouse and not G.CONTROLLER.HID.axis_cursor) then
|
|
else
|
|
self:L_cursor_press()
|
|
end
|
|
end
|
|
if button == 'b' then
|
|
if G.hand and self.focused.target and
|
|
self.focused.target.area == G.hand then
|
|
self:queue_R_cursor_press()
|
|
else
|
|
self.interrupt.focus = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Controller:button_hold_update(button, dt)
|
|
if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
|
|
self.frame_buttonpress = true
|
|
if self.held_button_times[button] then
|
|
self.held_button_times[button] = self.held_button_times[button] + dt
|
|
self:capture_focused_input(button, 'hold', dt)
|
|
end
|
|
if (button == 'dpleft' or button == 'dpright' or button == 'dpup' or button == 'dpdown') and not self.no_holdcap then
|
|
self.repress_timer = self.repress_timer or 0.3
|
|
if self.held_button_times[button] and (self.held_button_times[button] > self.repress_timer) then
|
|
self.repress_timer = 0.1
|
|
self.held_button_times[button] = 0
|
|
self:button_press_update(button, dt)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Controller:button_release_update(button, dt)
|
|
if not self.held_button_times[button] then return end
|
|
self.repress_timer = 0.3
|
|
self.held_button_times[button] = nil
|
|
if button == 'a' then
|
|
self:L_cursor_release()
|
|
end
|
|
end
|
|
|
|
function Controller:key_press_update(key, dt)
|
|
if key == "escape" and Cartomancer.INTERNAL_in_config then
|
|
Cartomancer.INTERNAL_in_config = false
|
|
if not Cartomancer.use_smods() then
|
|
Cartomancer.save_config()
|
|
end
|
|
end
|
|
if key == "escape" and G.ACTIVE_MOD_UI then
|
|
G.FUNCS.exit_mods()
|
|
end
|
|
if self.locks.frame then return end
|
|
if string.sub(key, 1, 2) == 'kp' then key = string.sub(key, 3) end
|
|
if key == 'enter' then key = 'return' end
|
|
|
|
if self.text_input_hook then
|
|
if key == "escape" then
|
|
self.text_input_hook = nil
|
|
elseif key == "capslock" then
|
|
self.capslock = not self.capslock
|
|
else
|
|
G.FUNCS.text_input_key{
|
|
e=self.text_input_hook,
|
|
key = key,
|
|
caps = self.held_keys["lshift"] or self.held_keys["rshift"]
|
|
}
|
|
end
|
|
return
|
|
end
|
|
|
|
if key == "escape" then
|
|
if G.STATE == G.STATES.SPLASH then
|
|
G:delete_run()
|
|
G:main_menu()
|
|
else
|
|
if not G.OVERLAY_MENU then
|
|
G.FUNCS:options()
|
|
elseif not G.OVERLAY_MENU.config.no_esc then
|
|
G.FUNCS:exit_overlay_menu()
|
|
end
|
|
end
|
|
end
|
|
|
|
if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
|
|
self.frame_buttonpress = true
|
|
self.held_key_times[key] = 0
|
|
|
|
|
|
for _, keybind in pairs(SMODS.Keybinds) do
|
|
if keybind.action and keybind.key_pressed == key and keybind.event == 'pressed' then
|
|
local execute = true
|
|
for _, other_key in pairs(keybind.held_keys) do
|
|
if not self.held_keys[other_key] then
|
|
execute = false
|
|
break
|
|
end
|
|
end
|
|
if execute then
|
|
keybind:action()
|
|
end
|
|
end
|
|
end
|
|
if not _RELEASE_MODE and require("debugplus.core").isOkayToHandleDebugForKey(key) then
|
|
if key == 'tab' and not G.debug_tools then
|
|
G.debug_tools = UIBox{
|
|
definition = create_UIBox_debug_tools(),
|
|
config = {align='cr', offset = {x=G.ROOM.T.x + 11,y=0},major = G.ROOM_ATTACH, bond = 'Weak'}
|
|
}
|
|
G.E_MANAGER:add_event(Event({
|
|
blockable = false,
|
|
func = function()
|
|
G.debug_tools.alignment.offset.x = -4
|
|
return true
|
|
end
|
|
}))
|
|
end
|
|
if self.hovering.target and self.hovering.target:is(Card) then
|
|
local _card = self.hovering.target
|
|
if G.OVERLAY_MENU then
|
|
if key == "1" then
|
|
unlock_card(_card.config.center)
|
|
_card:set_sprites(_card.config.center)
|
|
end
|
|
if key == "2" then
|
|
unlock_card(_card.config.center)
|
|
discover_card(_card.config.center)
|
|
_card:set_sprites(_card.config.center)
|
|
end
|
|
if key == "3" then
|
|
if _card.ability.set == 'Joker' and G.jokers and #G.jokers.cards < G.jokers.config.card_limit then
|
|
add_joker(_card.config.center.key)
|
|
_card:set_sprites(_card.config.center)
|
|
end
|
|
local debugplus = require("debugplus.core")
|
|
debugplus.handleSpawn(self, _card)
|
|
if _card.ability.consumeable and G.consumeables and #G.consumeables.cards < G.consumeables.config.card_limit then
|
|
add_joker(_card.config.center.key)
|
|
_card:set_sprites(_card.config.center)
|
|
end
|
|
end
|
|
end
|
|
if key == 'q' then
|
|
if (_card.ability.set == 'Joker' or _card.playing_card or _card.area) then
|
|
local found_index = 1
|
|
if _card.edition then
|
|
for i, v in ipairs(G.P_CENTER_POOLS.Edition) do
|
|
if v.key == _card.edition.key then
|
|
found_index = i
|
|
break
|
|
end
|
|
end
|
|
end
|
|
found_index = found_index + 1
|
|
if found_index > #G.P_CENTER_POOLS.Edition then found_index = found_index - #G.P_CENTER_POOLS.Edition end
|
|
local _edition = G.P_CENTER_POOLS.Edition[found_index].key
|
|
_card:set_edition(_edition, true, true)
|
|
end
|
|
end
|
|
end
|
|
if key == 'h' then
|
|
G.debug_UI_toggle = not G.debug_UI_toggle
|
|
end
|
|
if key == 'b' then
|
|
G:delete_run()
|
|
G:start_run({})
|
|
end
|
|
if key == 'l' then
|
|
G:delete_run()
|
|
G.SAVED_GAME = get_compressed(G.SETTINGS.profile..'/'..'save.jkr')
|
|
if G.SAVED_GAME ~= nil then G.SAVED_GAME = STR_UNPACK(G.SAVED_GAME) end
|
|
G:start_run({savetext = G.SAVED_GAME})
|
|
end
|
|
if key == 'j' then
|
|
G.debug_splash_size_toggle = not G.debug_splash_size_toggle
|
|
G:delete_run()
|
|
G:main_menu('splash')
|
|
end
|
|
if key == '8' then
|
|
love.mouse.setVisible( not love.mouse.isVisible() )
|
|
end
|
|
if key == '9' then
|
|
G.debug_tooltip_toggle = not G.debug_tooltip_toggle
|
|
end
|
|
if key == "space" then
|
|
live_test()
|
|
end
|
|
local debugplus = require("debugplus.core")
|
|
debugplus.handleKeys(self, key, dt)
|
|
if key == 'v' then
|
|
if not G.prof then G.prof = require "engine/profile"; G.prof.start()
|
|
else G.prof:stop();
|
|
print(G.prof.report()); G.prof = nil end
|
|
debugplus.profileMessage()
|
|
end
|
|
if key == "p" then
|
|
G.SETTINGS.perf_mode = not G.SETTINGS.perf_mode
|
|
debugplus.togglePerfUI()
|
|
end
|
|
end
|
|
end
|
|
|
|
function Controller:key_hold_update(key, dt)
|
|
if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
|
|
--self.frame_buttonpress = true
|
|
if self.held_key_times[key] then
|
|
for _, keybind in pairs(SMODS.Keybinds) do
|
|
if keybind.key_pressed == key and keybind.event == 'held' and keybind.held_duration then
|
|
if self.held_key_times[key] > keybind.held_duration then
|
|
local execute = true
|
|
for _, other_key in pairs(keybind.held_keys) do
|
|
if not self.held_keys[other_key] then
|
|
execute = false
|
|
break
|
|
end
|
|
end
|
|
if execute then
|
|
keybind:action()
|
|
self.held_key_times[key] = nil
|
|
end
|
|
else
|
|
self.held_key_times[key] = self.held_key_times[key] + dt
|
|
end
|
|
end
|
|
end
|
|
if key == "r" and not G.SETTINGS.paused and not (G.GAME and G.GAME.USING_CODE) then
|
|
if self.held_key_times[key] > 0.7 then
|
|
if not G.GAME.won and not G.GAME.seeded and not G.GAME.challenge then
|
|
G.PROFILES[G.SETTINGS.profile].high_scores.current_streak.amt = 0
|
|
end
|
|
G:save_settings()
|
|
self.held_key_times[key] = nil
|
|
G.SETTINGS.current_setup = 'New Run'
|
|
G.GAME.viewed_back = nil
|
|
G.run_setup_seed = G.GAME.seeded
|
|
G.challenge_tab = G.GAME and G.GAME.challenge and G.GAME.challenge_tab or nil
|
|
G.forced_seed, G.setup_seed = nil, nil
|
|
if G.GAME.seeded then G.forced_seed = G.GAME.pseudorandom.seed end
|
|
G.forced_stake = G.GAME.stake
|
|
if G.STAGE == G.STAGES.RUN then G.FUNCS.start_setup_run() end
|
|
G.forced_stake = nil
|
|
G.challenge_tab = nil
|
|
G.forced_seed = nil
|
|
else
|
|
self.held_key_times[key] = self.held_key_times[key] + dt
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Controller:key_release_update(key, dt)
|
|
for _, keybind in pairs(SMODS.Keybinds) do
|
|
if keybind.action and keybind.key_pressed == key and keybind.event == 'released' then
|
|
local execute = true
|
|
for _, other_key in pairs(keybind.held_keys) do
|
|
if not self.held_keys[other_key] then
|
|
execute = false
|
|
break
|
|
end
|
|
end
|
|
if execute then
|
|
keybind:action()
|
|
end
|
|
end
|
|
end
|
|
if ((self.locked) and not G.SETTINGS.paused) or (self.locks.frame) or (self.frame_buttonpress) then return end
|
|
self.frame_buttonpress = true
|
|
if key == "a" and self.held_keys["g"] and not _RELEASE_MODE then
|
|
G.DEBUG = not(G.DEBUG)
|
|
end
|
|
if key == 'tab' and G.debug_tools then
|
|
G.debug_tools:remove()
|
|
G.debug_tools = nil
|
|
end
|
|
end
|
|
|
|
function Controller:key_press(key)
|
|
self.pressed_keys[key] = true
|
|
self.held_keys[key] = true
|
|
end
|
|
|
|
function Controller:key_release(key)
|
|
self.held_keys[key] = nil
|
|
self.released_keys[key] = true
|
|
end
|
|
|
|
function Controller:button_press(button)
|
|
self.pressed_buttons[button] = true
|
|
self.held_buttons[button] = true
|
|
end
|
|
|
|
function Controller:button_release(button)
|
|
self.held_buttons[button] = nil
|
|
self.released_buttons[button] = true
|
|
end
|
|
|
|
function Controller:get_cursor_collision(cursor_trans)
|
|
self.collision_list = EMPTY(self.collision_list)
|
|
self.nodes_at_cursor = EMPTY(self.nodes_at_cursor)
|
|
|
|
if self.COYOTE_FOCUS then return end
|
|
if self.dragging.target then
|
|
self.dragging.target.states.collide.is = true
|
|
self.nodes_at_cursor[#self.nodes_at_cursor+1] = self.dragging.target
|
|
self.collision_list[#self.collision_list+1] = self.dragging.target
|
|
end
|
|
|
|
if not G.DRAW_HASH[1] or
|
|
cursor_trans.x-G.ROOM.T.x < -G.DRAW_HASH_BUFF or cursor_trans.x-G.ROOM.T.x > G.TILE_W + G.DRAW_HASH_BUFF or
|
|
cursor_trans.y-G.ROOM.T.y < -G.DRAW_HASH_BUFF or cursor_trans.y-G.ROOM.T.y > G.TILE_H + G.DRAW_HASH_BUFF then
|
|
return
|
|
end
|
|
|
|
local DRAW_HASH_SQUARE = G.DRAW_HASH
|
|
for i = #DRAW_HASH_SQUARE, 1, -1 do
|
|
local v = DRAW_HASH_SQUARE[i]
|
|
if v:collides_with_point(cursor_trans) and not v.REMOVED then
|
|
self.nodes_at_cursor[#self.nodes_at_cursor+1] = v
|
|
if v.states.collide.can then
|
|
v.states.collide.is = true
|
|
self.collision_list[#self.collision_list+1] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Controller:set_cursor_hover()
|
|
self.cursor_hover.T = self.cursor_hover.T or {}
|
|
self.cursor_hover.T.x, self.cursor_hover.T.y =G.CURSOR.T.x, G.CURSOR.T.y
|
|
self.cursor_hover.time = G.TIMERS.TOTAL
|
|
|
|
self.cursor_hover.prev_target = self.cursor_hover.target
|
|
self.cursor_hover.target = nil
|
|
|
|
if self.interrupt.focus or ((self.locked) and (not G.SETTINGS.paused or G.screenwipe)) or self.locks.frame or self.COYOTE_FOCUS then self.cursor_hover.target = G.ROOM; return end
|
|
|
|
if self.HID.controller and self.focused.target and self.focused.target.states.hover.can then
|
|
if (self.HID.dpad or self.HID.axis_cursor) and self.focused.target.states.collide.is then
|
|
self.cursor_hover.target = self.focused.target
|
|
else
|
|
for _, v in ipairs(self.collision_list) do
|
|
if v.states.hover.can then
|
|
self.cursor_hover.target = v
|
|
break
|
|
end
|
|
end
|
|
end
|
|
else
|
|
for _, v in ipairs(self.collision_list) do
|
|
if v.states.hover.can and (not v.states.drag.is or self.HID.touch) then
|
|
self.cursor_hover.target = v
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not self.cursor_hover.target or (self.dragging.target and not self.HID.touch) then self.cursor_hover.target = G.ROOM end
|
|
if self.cursor_hover.target ~= self.cursor_hover.prev_target then self.cursor_hover.handled = false end
|
|
end
|
|
|
|
function Controller:queue_L_cursor_press(x, y)
|
|
if self.locks.frame then return end
|
|
if G.STATE == G.STATES.SPLASH then
|
|
self:key_press('escape')
|
|
end
|
|
self.L_cursor_queue = {x = x, y = y}
|
|
end
|
|
|
|
function Controller:queue_R_cursor_press(x, y)
|
|
if self.locks.frame then return end
|
|
if not G.SETTINGS.paused and G.hand and G.hand.highlighted[1] then
|
|
if (G.play and #G.play.cards > 0) or
|
|
(self.locked) or
|
|
(self.locks.frame) or
|
|
(G.GAME.STOP_USE and G.GAME.STOP_USE > 0) then return end
|
|
G.hand:unhighlight_all()
|
|
end
|
|
end
|
|
|
|
function Controller:L_cursor_press(x, y)
|
|
x = x or self.cursor_position.x
|
|
y = y or self.cursor_position.y
|
|
|
|
if ((self.locked) and (not G.SETTINGS.paused or G.screenwipe)) or (self.locks.frame) then return end
|
|
|
|
self.cursor_down.T = {x = x/(G.TILESCALE*G.TILESIZE), y = y/(G.TILESCALE*G.TILESIZE)}
|
|
self.cursor_down.time = G.TIMERS.TOTAL
|
|
self.cursor_down.handled = false
|
|
self.cursor_down.target = nil
|
|
self.is_cursor_down = true
|
|
|
|
local press_node = (self.HID.touch and self.cursor_hover.target) or self.hovering.target or self.focused.target
|
|
|
|
if press_node then
|
|
self.cursor_down.target = press_node.states.click.can and press_node or press_node:can_drag() or nil
|
|
end
|
|
|
|
if self.cursor_down.target == nil then
|
|
self.cursor_down.target = G.ROOM
|
|
end
|
|
end
|
|
|
|
function Controller:L_cursor_release(x, y)
|
|
x = x or self.cursor_position.x
|
|
y = y or self.cursor_position.y
|
|
|
|
if ((self.locked) and (not G.SETTINGS.paused or G.screenwipe)) or (self.locks.frame) then return end
|
|
|
|
self.cursor_up.T = {x = x/(G.TILESCALE*G.TILESIZE), y = y/(G.TILESCALE*G.TILESIZE)}
|
|
self.cursor_up.time = G.TIMERS.TOTAL
|
|
self.cursor_up.handled = false
|
|
self.cursor_up.target = nil
|
|
self.is_cursor_down = false
|
|
|
|
self.cursor_up.target = self.hovering.target or self.focused.target
|
|
|
|
if self.cursor_up.target == nil then
|
|
self.cursor_up.target = G.ROOM
|
|
end
|
|
end
|
|
|
|
function Controller:is_node_focusable(node)
|
|
local ret_val = false
|
|
if node.T.y > G.ROOM.T.h + 3 then return false end
|
|
if not node.REMOVED and not node.under_overlay and (node.states.hover.can and not self.dragging.target or self.dragging.target == node) and
|
|
(not not node.created_on_pause) == (not not G.SETTINGS.paused) and
|
|
(node.states.visible) and (not node.UIBox or node.UIBox.states.visible) then
|
|
if self.screen_keyboard then
|
|
if node.UIBox and node.UIBox == self.screen_keyboard and node.config.button then
|
|
ret_val = true
|
|
end
|
|
else
|
|
if node:is(Card) and (node.facing == 'front' or node.area == G.hand or node.area == G.jokers or (node == G.deck)) and
|
|
node.states.hover.can and not node.jimbo then
|
|
ret_val = true
|
|
end
|
|
if node.config and node.config.force_focus then ret_val = true end
|
|
if node.config and node.config.button then ret_val = true end
|
|
if node.config and node.config.focus_args then
|
|
if node.config.focus_args.type == 'none' or node.config.focus_args.funnel_from then
|
|
ret_val = false
|
|
else
|
|
ret_val = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return ret_val
|
|
end
|
|
|
|
--essentially works like 'hovering' with any nodes that are focusable, but
|
|
--the nodes can also be navigated to via controller key inputs. If no direction is supplied,
|
|
--this function focuses on any nodes that collide with this cursor. If a direction is
|
|
--supplied, focuses on the nearest node in that direction.
|
|
function Controller:update_focus(dir)
|
|
self.focused.prev_target = self.focused.target
|
|
|
|
--Only needed when using a controller, hovering covers all KBM scenarios
|
|
if not self.HID.controller or self.interrupt.focus or (self.locked) and (not G.SETTINGS.paused or G.screenwipe) then
|
|
if self.focused.target then self.focused.target.states.focus.is = false end
|
|
self.focused.target = nil
|
|
return
|
|
end
|
|
|
|
G.ARGS.focus_list = EMPTY(G.ARGS.focus_list)
|
|
G.ARGS.focusables = EMPTY(G.ARGS.focusables)
|
|
|
|
-------------------------------------------------
|
|
-- Find the focusable starting point
|
|
-------------------------------------------------
|
|
--First, is there currently a focusable target that is still valid?
|
|
if self.focused.target then
|
|
self.focused.target.states.focus.is = false
|
|
|
|
--If that node is no longer focusable or if the cursor no longer collides with the node, remove the target
|
|
if not self:is_node_focusable(self.focused.target) or not self.focused.target:collides_with_point(G.CURSOR.T) or self.HID.axis_cursor then
|
|
self.focused.target = nil
|
|
end
|
|
end
|
|
|
|
--Now we check for 3 criteria:
|
|
--1: is there a current focused target and no dpad direction? if so, we simply add the currsnt focused target to the focusable list and set the state to true
|
|
--2: if not, and there is no dpad direction, iterate through the node list that the cursor intersects and check if any are focusable, only add the first one
|
|
--3: if there is a direction, add all focusable moveables to the focusable list to check later
|
|
if not dir and self.focused.target then
|
|
self.focused.target.states.focus.can = true
|
|
G.ARGS.focusables[#G.ARGS.focusables+1] = self.focused.target
|
|
else
|
|
if not dir then
|
|
for k, v in ipairs(self.nodes_at_cursor) do
|
|
v.states.focus.can = false
|
|
v.states.focus.is = false
|
|
if #G.ARGS.focusables == 0 and self:is_node_focusable(v) then
|
|
v.states.focus.can = true
|
|
G.ARGS.focusables[#G.ARGS.focusables+1] = v
|
|
end
|
|
end
|
|
else
|
|
for k, v in pairs(G.MOVEABLES) do
|
|
v.states.focus.can = false
|
|
v.states.focus.is = false
|
|
if self:is_node_focusable(v) then
|
|
v.states.focus.can = true
|
|
G.ARGS.focusables[#G.ARGS.focusables+1] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--If there are any valid focusables
|
|
if #G.ARGS.focusables > 0 then
|
|
--If a direction control is supplied, set the target to be the closest node in that direction
|
|
if dir then
|
|
if (dir == 'L' or dir == 'R') and self.focused.target and self.focused.target:is(Card) and self.focused.target.area == G.hand and G.hand then
|
|
local nu_rank = self.focused.target.rank + (dir == 'L' and -1 or 1)
|
|
if nu_rank > #G.hand.cards then nu_rank = 1 end
|
|
if nu_rank == 0 then nu_rank = #G.hand.cards end
|
|
if nu_rank ~= self.focused.target.rank then G.ARGS.focus_list[1] = {node = G.hand.cards[nu_rank]} end
|
|
else
|
|
--set the cursor position to where it currently is on screen
|
|
G.ARGS.focus_cursor_pos = G.ARGS.focus_cursor_pos or {}
|
|
G.ARGS.focus_cursor_pos.x, G.ARGS.focus_cursor_pos.y = G.CURSOR.T.x - G.ROOM.T.x, G.CURSOR.T.y - G.ROOM.T.y
|
|
|
|
--if there is a focused target, set the cursor to the midpoint
|
|
if self.focused.target then
|
|
_ft = self.focused.target
|
|
if self.focused.target.config.focus_args and self.focused.target.config.focus_args.funnel_to then
|
|
_ft = self.focused.target.config.focus_args.funnel_to
|
|
end
|
|
G.ARGS.focus_cursor_pos.x, G.ARGS.focus_cursor_pos.y = _ft.T.x + 0.5*_ft.T.w,_ft.T.y + 0.5*_ft.T.h
|
|
--if not but there is a focusable hovering target, put the cursor on it instead
|
|
elseif self.hovering.target and self.hovering.target.states.focus.can then
|
|
G.ARGS.focus_cursor_pos.x, G.ARGS.focus_cursor_pos.y = self.hovering.target:put_focused_cursor()
|
|
G.ARGS.focus_cursor_pos.x = G.ARGS.focus_cursor_pos.x / (G.TILESCALE*G.TILESIZE) - G.ROOM.T.x
|
|
G.ARGS.focus_cursor_pos.y = G.ARGS.focus_cursor_pos.y / (G.TILESCALE*G.TILESIZE) - G.ROOM.T.y
|
|
end
|
|
|
|
--set the list to be all the nodes in that direction sorted by the closest node
|
|
for _, v in pairs(G.ARGS.focusables) do
|
|
if v ~= self.hovering.target and v ~= self.focused.target then
|
|
local eligible = false
|
|
|
|
if v.config.focus_args and v.config.focus_args.funnel_to then
|
|
v = v.config.focus_args.funnel_to
|
|
end
|
|
|
|
G.ARGS.focus_vec = G.ARGS.focus_vec or {}
|
|
G.ARGS.focus_vec.x = v.T.x + 0.5*v.T.w - (G.ARGS.focus_cursor_pos.x)
|
|
G.ARGS.focus_vec.y = v.T.y + 0.5*v.T.h - (G.ARGS.focus_cursor_pos.y)
|
|
|
|
if v.config.focus_args and v.config.focus_args.nav then
|
|
if v.config.focus_args.nav == 'wide' then
|
|
if G.ARGS.focus_vec.y > 0.1 and dir == 'D' then eligible = true
|
|
elseif G.ARGS.focus_vec.y < -0.1 and dir == 'U' then eligible = true
|
|
elseif math.abs(G.ARGS.focus_vec.y) < v.T.h/2 then eligible = true end
|
|
elseif v.config.focus_args.nav == 'tall' then
|
|
if G.ARGS.focus_vec.x > 0.1 and dir == 'R' then eligible = true
|
|
elseif G.ARGS.focus_vec.x < -0.1 and dir == 'L' then eligible = true
|
|
elseif math.abs(G.ARGS.focus_vec.x) < v.T.w/2 then eligible = true end
|
|
end
|
|
elseif math.abs(G.ARGS.focus_vec.x) > math.abs(G.ARGS.focus_vec.y) then
|
|
if G.ARGS.focus_vec.x > 0 and dir == 'R' then eligible = true
|
|
elseif G.ARGS.focus_vec.x < 0 and dir == 'L' then eligible = true end
|
|
else
|
|
if G.ARGS.focus_vec.y > 0 and dir == 'D' then eligible = true
|
|
elseif G.ARGS.focus_vec.y < 0 and dir == 'U' then eligible = true end
|
|
end
|
|
|
|
if eligible then
|
|
G.ARGS.focus_list[#G.ARGS.focus_list+1] = {node = v, dist = math.abs(G.ARGS.focus_vec.x) + math.abs(G.ARGS.focus_vec.y)}
|
|
end
|
|
end
|
|
end
|
|
if #G.ARGS.focus_list < 1 then
|
|
if self.focused.target then self.focused.target.states.focus.is = true end
|
|
return
|
|
end
|
|
table.sort(G.ARGS.focus_list, function (a, b) return a.dist < b.dist end)
|
|
end
|
|
else
|
|
if self.focused.target then
|
|
G.ARGS.focus_list[#G.ARGS.focus_list+1] = {node = self.focused.target, dist = 0}
|
|
else
|
|
--otherwise, get the focusable that collides
|
|
G.ARGS.focus_list[#G.ARGS.focus_list+1] = {node = G.ARGS.focusables[1], dist = 0}
|
|
end
|
|
end
|
|
end
|
|
|
|
--now with the lists created, set the focused target to be the first node in the list
|
|
if G.ARGS.focus_list[1] then
|
|
if G.ARGS.focus_list[1].node.config and G.ARGS.focus_list[1].node.config.focus_args and G.ARGS.focus_list[1].node.config.focus_args.funnel_from then
|
|
self.focused.target = G.ARGS.focus_list[1].node.config.focus_args.funnel_from
|
|
else
|
|
self.focused.target = G.ARGS.focus_list[1].node
|
|
end
|
|
if self.focused.target ~= self.focused.prev_target then G.VIBRATION = G.VIBRATION + 0.7 end
|
|
else
|
|
self.focused.target = nil
|
|
end
|
|
|
|
if self.focused.target then self.focused.target.states.focus.is = true end
|
|
end
|
|
|
|
function Controller:capture_focused_input(button, input_type, dt)
|
|
local ret = false
|
|
local focused = self.focused.target
|
|
local extern_button = false
|
|
self.no_holdcap = nil
|
|
|
|
--Implementing 'coyote time' type selection where a full button press isnt needed to select a card in hand. As long as a button down has been registered
|
|
--before a timer is up and the dpad is used to move to the next card it should register
|
|
if input_type == 'press' and (button == 'dpleft' or button == 'dpright') and
|
|
focused and self.dragging.target and
|
|
(self.held_button_times['a'] and self.held_button_times['a'] < 0.12) and
|
|
focused.area and focused.area:can_highlight(focused) then
|
|
self:L_cursor_release()
|
|
self:navigate_focus(button == 'dpleft' and 'L' or 'R')
|
|
self.held_button_times['a'] = nil
|
|
self.COYOTE_FOCUS = true
|
|
ret = true
|
|
elseif input_type == 'press' and focused and focused.area and focused == self.dragging.target then
|
|
focused.states.drag.is = false
|
|
if button == 'dpleft' and focused.rank > 1 then
|
|
focused.rank = focused.rank - 1
|
|
focused.area.cards[focused.rank].rank = focused.rank + 1
|
|
table.sort(focused.area.cards, function (a, b) return a.rank < b.rank end)
|
|
focused.area:align_cards()
|
|
self:update_cursor()
|
|
elseif button == 'dpright' and focused.rank < #focused.area.cards then
|
|
focused.rank = focused.rank + 1
|
|
focused.area.cards[focused.rank].rank = focused.rank - 1
|
|
table.sort(focused.area.cards, function (a, b) return a.rank < b.rank end)
|
|
focused.area:align_cards()
|
|
self:update_cursor()
|
|
end
|
|
focused.states.drag.is = true
|
|
ret = true
|
|
end
|
|
|
|
if G.OVERLAY_MENU and not self.screen_keyboard and input_type == 'press' and G.OVERLAY_MENU:get_UIE_by_ID('tab_shoulders') and (button == 'leftshoulder' or button == 'rightshoulder') then
|
|
focused = G.OVERLAY_MENU:get_UIE_by_ID('tab_shoulders')
|
|
extern_button = true
|
|
end
|
|
if G.OVERLAY_MENU and not self.screen_keyboard and input_type == 'press' and G.OVERLAY_MENU:get_UIE_by_ID('cycle_shoulders') and (button == 'leftshoulder' or button == 'rightshoulder') then
|
|
focused = G.OVERLAY_MENU:get_UIE_by_ID('cycle_shoulders').children[1]
|
|
extern_button = true
|
|
end
|
|
if focused and focused.config.focus_args then
|
|
if focused.config.focus_args.type == 'cycle' and input_type == 'press' then
|
|
if ((extern_button and button == 'leftshoulder') or (not extern_button and button == 'dpleft')) then
|
|
focused.children[1]:click()
|
|
ret = true
|
|
end
|
|
if ((extern_button and button == 'rightshoulder') or (not extern_button and button == 'dpright')) then
|
|
focused.children[3]:click()
|
|
ret = true
|
|
end
|
|
end
|
|
if focused.config.focus_args.type == 'tab' and input_type == 'press' then
|
|
local proto_choices = focused.UIBox:get_group(nil, focused.children[1].children[1].config.group)
|
|
local choices = {}
|
|
for k, v in ipairs(proto_choices) do
|
|
if v.config.choice and v.config.button then choices[#choices+1] = v end
|
|
end
|
|
for k, v in ipairs(choices) do
|
|
if v.config.chosen then
|
|
if ((extern_button and button == 'leftshoulder') or (not extern_button and button == 'dpleft')) then
|
|
local next_i = k ~= 1 and (k-1) or (#choices)
|
|
if focused.config.focus_args.no_loop and next_i > k then ret = nil
|
|
else
|
|
choices[next_i]:click()
|
|
self:snap_to({node = choices[next_i]})
|
|
self:update_cursor()
|
|
ret = true
|
|
end
|
|
elseif ((extern_button and button == 'rightshoulder') or (not extern_button and button == 'dpright')) then
|
|
local next_i = k ~= #choices and (k+1) or (1)
|
|
if focused.config.focus_args.no_loop and next_i < k then ret = nil
|
|
else
|
|
choices[next_i]:click()
|
|
self:snap_to({node = choices[next_i]})
|
|
self:update_cursor()
|
|
ret = true
|
|
end
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if focused.config.focus_args.type == 'slider' then
|
|
if button == 'dpleft' then
|
|
self.no_holdcap = true
|
|
if input_type == 'hold' and self.held_button_times[button] > 0.2 then
|
|
G.FUNCS.slider_descreet(focused.children[1], -dt*self.held_button_times[button]*0.6)
|
|
end
|
|
if input_type == 'press' then
|
|
G.FUNCS.slider_descreet(focused.children[1], -0.01)
|
|
end
|
|
ret = true
|
|
end
|
|
if button == 'dpright' then
|
|
self.no_holdcap = true
|
|
if input_type == 'hold' and self.held_button_times[button] > 0.2 then
|
|
G.FUNCS.slider_descreet(focused.children[1], dt*self.held_button_times[button]*0.6)
|
|
end
|
|
if input_type == 'press' then
|
|
G.FUNCS.slider_descreet(focused.children[1], 0.01)
|
|
end
|
|
ret = true
|
|
end
|
|
end
|
|
end
|
|
if ret == true then G.VIBRATION = G.VIBRATION +1 end
|
|
return ret
|
|
end
|
|
|
|
function Controller:navigate_focus(dir)
|
|
--Get the corresponding focus target first, with or without a direction
|
|
self:update_focus(dir)
|
|
|
|
--Set the cursor to be in the correct position for that target
|
|
self:update_cursor()
|
|
end
|
|
|