diff --git a/src/core/Game.lua b/src/core/Game.lua index 1caef072..02a7b7f2 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -9,6 +9,7 @@ local Renderer = require("src.render.Renderer") local SaveData = require("src.core.SaveData") local StateStack = require("src.core.StateStack") local TouchControls = require("src.core.TouchControls") +local GamepadMap = require("src.core.GamepadMap") local ModLoader = require("src.mods.Loader") local ModRuntime = require("src.mods.Runtime") local Screens = require("src.ui.Screens") @@ -489,6 +490,23 @@ function Game:gamepadpressed(joystick, button) top:onGamepadPressed(button) return end + -- Select+face display chords → same digit path as Game:keypressed + -- (COLORS/TILT/pipelines). Intercept before Input so face does not + -- also fire GB A/B. Dual-path: raw already ignored when isGamepad(). + local selectHeld = Input:isDown("select") + if not selectHeld and joystick and joystick.isGamepadDown then + local ok, down = pcall(function() + return joystick:isGamepadDown("back") + end) + selectHeld = ok and down == true + end + if selectHeld then + local digit = GamepadMap.displayChordDigit(button) + if digit then + self:keypressed(digit) + return + end + end Input:gamepadpressed(joystick, button) end diff --git a/tests/engine/game_display_chord_test.lua b/tests/engine/game_display_chord_test.lua new file mode 100644 index 00000000..02f478b3 --- /dev/null +++ b/tests/engine/game_display_chord_test.lua @@ -0,0 +1,122 @@ +-- Select+face chords fire Game:keypressed digits (NXMOD-06..10). +-- Spec: Select held + A/B/Y/X/L → keys 2/3/5/6/7; without Select, face stays GB. +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local T = require("tests.harness") +local check = T.check +local eq = T.eq + +local Game = require("src.core.Game") +local Input = require("src.core.Input") +local GamepadMap = require("src.core.GamepadMap") + +local joy = { + isGamepad = function() return true end, + isGamepadDown = function(_, button) return false end, +} + +local digits = {} +local padForwarded = {} +local wroteOptions = false + +local origKeypressed = Game.keypressed +local origPad = Input.gamepadpressed +local origWrite = Game.writeOptions + +function Game:keypressed(key) + digits[#digits + 1] = key + -- Mimic digit persistence side-effect for keys that write options. + if key == "2" or key == "3" or key == "5" or key == "6" or key == "7" then + wroteOptions = true + end +end + +function Input:gamepadpressed(joystick, button) + padForwarded[#padForwarded + 1] = button + return origPad(self, joystick, button) +end + +local function resetSpies() + digits = {} + padForwarded = {} + wroteOptions = false +end + +local function holdSelect() + Input:init() + -- Press Select (SDL back) into Input so isDown("select") is true. + origPad(Input, joy, "back") + Input:step() + check(Input:isDown("select"), "Select held via Input") +end + +-- --- Without Select: face buttons keep normal GB path (NXMOD-09) --- +GamepadMap._setForceNXForTests(false) +Input:init() +resetSpies() +Game:gamepadpressed(joy, "a") +eq(#digits, 0, "no digit without Select") +eq(#padForwarded, 1, "face reaches Input without Select") +eq(padForwarded[1], "a", "Input receives face a") +check(not wroteOptions, "no options write without Select chord") + +-- --- Select + face → same digit path as PC keys (NXMOD-06..08, NXMOD-10) --- +local chordCases = { + { button = "a", digit = "2", label = "Select+A -> 2" }, + { button = "b", digit = "3", label = "Select+B -> 3" }, + { button = "y", digit = "5", label = "Select+Y -> 5" }, + { button = "x", digit = "6", label = "Select+X -> 6" }, + { button = "leftshoulder", digit = "7", label = "Select+L -> 7" }, +} + +for _, case in ipairs(chordCases) do + holdSelect() + resetSpies() + Game:gamepadpressed(joy, case.button) + eq(#digits, 1, case.label .. " fires keypressed once") + eq(digits[1], case.digit, case.label) + eq(#padForwarded, 0, case.label .. " does not forward face to Input") + check(wroteOptions, case.label .. " uses digit options path") +end + +-- --- NX Nintendo UX: physical A (SDL b) → 2, physical B (SDL a) → 3 --- +GamepadMap._setForceNXForTests(true) +holdSelect() +resetSpies() +Game:gamepadpressed(joy, "b") -- physical Nintendo A +eq(digits[1], "2", "NX Select+physical A (SDL b) -> key 2") +eq(#padForwarded, 0, "NX chord A does not forward face") +check(wroteOptions, "NX chord A options path") + +holdSelect() +resetSpies() +Game:gamepadpressed(joy, "a") -- physical Nintendo B +eq(digits[1], "3", "NX Select+physical B (SDL a) -> key 3") +eq(#padForwarded, 0, "NX chord B does not forward face") + +-- Without Select on NX, face still goes to Input (no accidental cycle). +Input:init() +resetSpies() +Game:gamepadpressed(joy, "b") +eq(#digits, 0, "NX no digit without Select") +eq(#padForwarded, 1, "NX face reaches Input without Select") + +-- Dual-path Select: joystick isGamepadDown("back") also counts as held. +GamepadMap._setForceNXForTests(false) +Input:init() +resetSpies() +local joySelectDown = { + isGamepad = function() return true end, + isGamepadDown = function(_, button) return button == "back" end, +} +Game:gamepadpressed(joySelectDown, "y") +eq(digits[1], "5", "Select via isGamepadDown(back) + Y -> 5") +eq(#padForwarded, 0, "isGamepadDown Select chord does not forward face") + +GamepadMap._setForceNXForTests(false) +Game.keypressed = origKeypressed +Input.gamepadpressed = origPad +Game.writeOptions = origWrite + +T.finish()