mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
fix(switch): restore Save Editor pad and touch input on NX
Route gamepad/touch into the editor instead of dropping them in editorMode, and hit-test clicks at event coords so a finger tap is not lost under the Joy-Con virtual cursor. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,6 +47,7 @@ the transfer runbook).
|
||||
- VoxelMod OPTIONS + Switch performance tips documented (WATER / 3D-BTL / extras)
|
||||
- Hardware evidence for Phase 0 probe, ROM import, naming A/B, save/suspend, fused NRO — see `docs/switch-hardware-evidence.md`
|
||||
- Path-gated CI selftest + canonical fused PR artifact; release Switch hard-fail
|
||||
- Save editor pad/touch input (virtual cursor, A click, B close) — see `tools/save-editor/README.md`
|
||||
|
||||
### Known gaps / welcome contributions
|
||||
|
||||
|
||||
@@ -369,49 +369,91 @@ end
|
||||
|
||||
function love.gamepadpressed(joystick, button)
|
||||
SwitchDiagnostics.onJoystickEvent("gamepadpressed", joystick, button)
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.gamepadpressed then
|
||||
return EditorApp.gamepadpressed(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:gamepadpressed(joystick, button) end
|
||||
Game:gamepadpressed(joystick, button)
|
||||
end
|
||||
|
||||
function love.gamepadreleased(joystick, button)
|
||||
SwitchDiagnostics.onJoystickEvent("gamepadreleased", joystick, button)
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.gamepadreleased then
|
||||
return EditorApp.gamepadreleased(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:gamepadreleased(joystick, button) end
|
||||
Game:gamepadreleased(joystick, button)
|
||||
end
|
||||
|
||||
function love.gamepadaxis(joystick, axis, value)
|
||||
SwitchDiagnostics.onJoystickEvent("gamepadaxis", joystick, axis, { value = value })
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.gamepadaxis then
|
||||
return EditorApp.gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
||||
Game:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function love.joystickpressed(joystick, button)
|
||||
SwitchDiagnostics.onJoystickEvent("joystickpressed", joystick, button)
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickpressed then
|
||||
return EditorApp.joystickpressed(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:joystickpressed(joystick, button) end
|
||||
Game:joystickpressed(joystick, button)
|
||||
end
|
||||
|
||||
function love.joystickreleased(joystick, button)
|
||||
SwitchDiagnostics.onJoystickEvent("joystickreleased", joystick, button)
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickreleased then
|
||||
return EditorApp.joystickreleased(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:joystickreleased(joystick, button) end
|
||||
Game:joystickreleased(joystick, button)
|
||||
end
|
||||
|
||||
function love.joystickaxis(joystick, axis, value)
|
||||
SwitchDiagnostics.onJoystickEvent("joystickaxis", joystick, axis, { value = value })
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickaxis then
|
||||
return EditorApp.joystickaxis(joystick, axis, value)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
||||
Game:joystickaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function love.joystickhat(joystick, hat, direction)
|
||||
SwitchDiagnostics.onJoystickEvent("joystickhat", joystick, hat, { direction = direction })
|
||||
if editorMode or TouchEditor then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickhat then
|
||||
return EditorApp.joystickhat(joystick, hat, direction)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
||||
Game:joystickhat(joystick, hat, direction)
|
||||
end
|
||||
@@ -459,7 +501,16 @@ function love.lowmemory()
|
||||
end
|
||||
|
||||
function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
if editorMode then return end
|
||||
if editorMode then
|
||||
-- iOS synthesizes mousepressed for the primary touch; forwarding here
|
||||
-- would double-fire. Android / NX need the explicit touch → click path
|
||||
-- (love-nx does not synthesize mouse for the editor the way desktop does).
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
if EditorApp and EditorApp.mousepressed then
|
||||
return EditorApp.mousepressed(x, y, 1)
|
||||
end
|
||||
return
|
||||
end
|
||||
if TouchEditor then
|
||||
-- iOS synthesizes mousepressed for the primary touch (same as the
|
||||
-- launcher); Android drives the editor through love.touch directly.
|
||||
@@ -537,6 +588,9 @@ function love.mousepressed(x, y, button, istouch)
|
||||
return Importer:mousepressed(x, y, button)
|
||||
end
|
||||
if editorMode and EditorApp.mousepressed then
|
||||
-- Same Android double-fire guard: touchpressed already clicked for the
|
||||
-- save editor; a synthesized mouse press must not fire again.
|
||||
if istouch and love.system.getOS() == "Android" then return end
|
||||
return EditorApp.mousepressed(x, y, button)
|
||||
end
|
||||
if mouseTouch and Game and button == 1 then
|
||||
|
||||
@@ -62,6 +62,7 @@ if grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/' "$LIST
|
||||
fi
|
||||
|
||||
for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \
|
||||
tools/save-editor/PadInput.lua \
|
||||
tools/save-editor/panels/Party.lua \
|
||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||
tools/rom_manifest_yellow.json; do
|
||||
|
||||
@@ -125,6 +125,7 @@ if [ -f data/generated/maps.lua ]; then
|
||||
run_tier "T3 save editor: map browser" "$LUA" tests/save_editor_task8_tests.lua
|
||||
run_tier "T3 save editor: mod awareness" "$LUA" tests/save_editor_mod_tests.lua
|
||||
run_tier "T3 save editor: wheel scrolling" "$LUA" tests/save_editor_wheel_bug595_test.lua
|
||||
run_tier "T3 save editor: pad / NX input" "$LUA" tests/save_editor_pad_input_test.lua
|
||||
run_tier "T5 link (loopback lockstep)" "$LUA" tests/run_link_tests.lua
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
-- Save editor pad / Joy-Con input (NX soft-lock fix).
|
||||
-- PadInput mirrors the launcher cursor; main.lua must forward editorMode
|
||||
-- gamepad/touch instead of discarding them.
|
||||
-- luajit tests/save_editor_pad_input_test.lua
|
||||
|
||||
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua"
|
||||
.. ";./tools/save-editor/panels/?.lua"
|
||||
|
||||
local love_stub = require("tests.love_stub")
|
||||
love = love or love_stub
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local GamepadMap = require("src.core.GamepadMap")
|
||||
local PadInput = require("PadInput")
|
||||
|
||||
PadInput.reset()
|
||||
|
||||
-- Stick deflection activates the cursor and moves it.
|
||||
PadInput.gamepadaxis(nil, "leftx", 1)
|
||||
PadInput.update(0.05)
|
||||
check(PadInput.isActive(), "left stick activates pad cursor")
|
||||
local x0 = select(1, PadInput.pointer())
|
||||
PadInput.update(0.05)
|
||||
local x1 = select(1, PadInput.pointer())
|
||||
check(x1 > x0, "left stick moves cursor right")
|
||||
|
||||
-- A / B via GamepadMap (desktop labels = GB a/b).
|
||||
eq(PadInput.gamepadpressed(nil, "a"), "a", "gamepad a → click action")
|
||||
eq(PadInput.gamepadpressed(nil, "b"), "b", "gamepad b → close action")
|
||||
eq(PadInput.gamepadpressed(nil, "leftshoulder"), "tab_prev", "L cycles tab prev")
|
||||
eq(PadInput.gamepadpressed(nil, "rightshoulder"), "tab_next", "R cycles tab next")
|
||||
|
||||
-- NX face swap: SDL south "a" is Nintendo B → GB b (close).
|
||||
GamepadMap._setForceNXForTests(true)
|
||||
eq(PadInput.gamepadpressed(nil, "a"), "b", "NX SDL a (south) → close (GB b)")
|
||||
eq(PadInput.gamepadpressed(nil, "b"), "a", "NX SDL b (east) → click (GB a)")
|
||||
GamepadMap._setForceNXForTests(false)
|
||||
|
||||
-- Dual-path gate: gamepad sticks must not also fire from raw joystick (#620).
|
||||
local gamepadJoy = {
|
||||
isGamepad = function() return true end,
|
||||
}
|
||||
eq(PadInput.joystickpressed(gamepadJoy, 1), nil,
|
||||
"ignoreRaw skips second fire on isGamepad sticks")
|
||||
|
||||
-- Raw (non-gamepad) stick still maps through GamepadMap.
|
||||
local rawJoy = {
|
||||
isGamepad = function() return false end,
|
||||
}
|
||||
eq(PadInput.joystickpressed(rawJoy, 1), "a",
|
||||
"raw #1 clicks via shared map")
|
||||
|
||||
-- Right stick accumulates wheel notches.
|
||||
PadInput.reset()
|
||||
PadInput.gamepadaxis(nil, "righty", -1) -- stick up
|
||||
PadInput.update(1.0)
|
||||
local notches = PadInput.takeWheel()
|
||||
check(notches >= 1, "right stick up yields positive wheel notches")
|
||||
|
||||
PadInput.reset()
|
||||
check(not PadInput.isActive(), "reset clears active cursor")
|
||||
|
||||
-- Touch / mouse must yield the pad so a tap is not hit-tested at the Joy-Con
|
||||
-- pointer (the NX "cursor shows but touch does nothing" bug).
|
||||
PadInput.gamepadaxis(nil, "leftx", 1)
|
||||
PadInput.update(0.05)
|
||||
check(PadInput.isActive(), "stick activates before yield")
|
||||
PadInput.yieldToPointer()
|
||||
check(not PadInput.isActive(), "yieldToPointer drops the virtual cursor")
|
||||
|
||||
-- Source seams: main.lua must forward editorMode gamepad/touch; App must
|
||||
-- own the pad handlers, prefer event click coords over the pad pointer, and
|
||||
-- feed pad coords into Kit only when active and no pending click.
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r"))
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
return src
|
||||
end
|
||||
|
||||
local mainSrc = read("main.lua")
|
||||
check(mainSrc:find("if editorMode then", 1, true) ~= nil
|
||||
and mainSrc:find("EditorApp.gamepadpressed", 1, true) ~= nil,
|
||||
"main.lua forwards gamepadpressed to EditorApp in editorMode")
|
||||
check(mainSrc:find("EditorApp.mousepressed(x, y, 1)", 1, true) ~= nil,
|
||||
"main.lua touchpressed clicks the save editor (non-iOS)")
|
||||
check(mainSrc:find('istouch and love.system.getOS() == "Android"', 1, true) ~= nil,
|
||||
"main.lua guards Android double-fire for editor mousepressed")
|
||||
|
||||
local appSrc = read("tools/save-editor/App.lua")
|
||||
check(appSrc:find('require("PadInput")', 1, true) ~= nil,
|
||||
"App.lua loads PadInput")
|
||||
check(appSrc:find("function App.gamepadpressed", 1, true) ~= nil,
|
||||
"App.lua exposes gamepadpressed")
|
||||
check(appSrc:find("PadInput.reset()", 1, true) ~= nil,
|
||||
"App.unload resets PadInput")
|
||||
check(appSrc:find("PadInput.pointer()", 1, true) ~= nil,
|
||||
"App.draw uses pad pointer when active")
|
||||
check(appSrc:find("PadInput.yieldToPointer()", 1, true) ~= nil,
|
||||
"App.mousepressed yields the pad so touch uses event coords")
|
||||
check(appSrc:find("mouseClicked and clickX", 1, true) ~= nil,
|
||||
"App.draw prefers click event coords over pad / mouse")
|
||||
|
||||
T.finish("save_editor_pad_input")
|
||||
@@ -24,6 +24,7 @@ local State = require("State")
|
||||
local Kit = require("Kit")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
local PadInput = require("PadInput")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
local Party = require("Party")
|
||||
@@ -41,6 +42,10 @@ local S
|
||||
-- vanilla records over an already-merged Data
|
||||
local mods
|
||||
local mouseClicked = false
|
||||
-- Click position from the press event. Kit samples the pointer in draw, so a
|
||||
-- touch / mouse / pad-A click must use the event coords -- not love.mouse
|
||||
-- (often stale on NX) and not the virtual cursor when a finger taps elsewhere.
|
||||
local clickX, clickY
|
||||
-- Wheel notches queued by App.wheelmoved since the last draw, handed to Kit
|
||||
-- there like mouseClicked is: LOVE delivers events before love.draw, so a
|
||||
-- notch is always spent by the frame that follows it (#595).
|
||||
@@ -233,6 +238,34 @@ function App.unload()
|
||||
-- deaf to every click (#541).
|
||||
Kit.blur()
|
||||
Kit.blockClicks = false
|
||||
PadInput.reset()
|
||||
end
|
||||
|
||||
local function cycleTab(delta)
|
||||
if not S then return end
|
||||
local idx = 1
|
||||
for i, t in ipairs(TABS) do
|
||||
if t.id == S.tab then idx = i; break end
|
||||
end
|
||||
idx = ((idx - 1 + delta) % #TABS) + 1
|
||||
S.tab = TABS[idx].id
|
||||
Ops.say(S, "Tab: " .. TABS[idx].label)
|
||||
end
|
||||
|
||||
-- Pad / Joy-Con actions from PadInput.gamepadpressed (A/B via GamepadMap so
|
||||
-- NX physical A confirms and B closes).
|
||||
local function handlePadAction(action)
|
||||
if not action or not S then return end
|
||||
if action == "a" then
|
||||
local mx, my = PadInput.pointer()
|
||||
App.mousepressed(mx, my, 1)
|
||||
elseif action == "b" then
|
||||
App.close()
|
||||
elseif action == "tab_prev" then
|
||||
cycleTab(-1)
|
||||
elseif action == "tab_next" then
|
||||
cycleTab(1)
|
||||
end
|
||||
end
|
||||
|
||||
function App.save()
|
||||
@@ -277,6 +310,7 @@ end
|
||||
-- host's onClose runs App.unload, which drops S -- doing that inline left the
|
||||
-- rest of the frame drawing against a nil state.
|
||||
function App.close()
|
||||
if not S then return false end
|
||||
if S.dirty and not S._quitArmed then
|
||||
S._quitArmed = true
|
||||
S.status = "Unsaved changes, Save first or click Close again to discard"
|
||||
@@ -301,16 +335,55 @@ function App.update(dt)
|
||||
-- directly in App.draw() via Kit.beginFrame. Tile animation (water,
|
||||
-- flowers) still needs ticking so the Map tab isn't static.
|
||||
TileRenderer.tick()
|
||||
PadInput.update(dt)
|
||||
local notches = PadInput.takeWheel()
|
||||
if notches ~= 0 then
|
||||
App.wheelmoved(0, notches)
|
||||
end
|
||||
end
|
||||
|
||||
function App.mousepressed(x, y, button)
|
||||
if button == 1 then mouseClicked = true end
|
||||
if button == 1 then
|
||||
mouseClicked = true
|
||||
clickX, clickY = x, y
|
||||
-- A finger / mouse tap yields the virtual cursor so the click lands where
|
||||
-- the event said, not under the Joy-Con pointer (NX touch soft-miss).
|
||||
PadInput.yieldToPointer()
|
||||
end
|
||||
end
|
||||
|
||||
function App.textinput(text)
|
||||
Kit.textinput(text)
|
||||
end
|
||||
|
||||
function App.gamepadpressed(joystick, button)
|
||||
handlePadAction(PadInput.gamepadpressed(joystick, button))
|
||||
end
|
||||
|
||||
function App.gamepadreleased(joystick, button)
|
||||
PadInput.gamepadreleased(joystick, button)
|
||||
end
|
||||
|
||||
function App.gamepadaxis(joystick, axis, value)
|
||||
PadInput.gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function App.joystickpressed(joystick, button)
|
||||
handlePadAction(PadInput.joystickpressed(joystick, button))
|
||||
end
|
||||
|
||||
function App.joystickreleased(joystick, button)
|
||||
PadInput.joystickreleased(joystick, button)
|
||||
end
|
||||
|
||||
function App.joystickaxis(joystick, axis, value)
|
||||
PadInput.joystickaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function App.joystickhat(joystick, hat, direction)
|
||||
PadInput.joystickhat(joystick, hat, direction)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ chrome
|
||||
-- The file chip: the single source of truth for "which file am I editing".
|
||||
-- The path truncates from the LEFT so the filename is always readable, and
|
||||
@@ -624,8 +697,15 @@ function App.draw()
|
||||
local s = Kit.scale
|
||||
|
||||
local mx, my = love.mouse.getPosition()
|
||||
local padX, padY, padOn = PadInput.pointer()
|
||||
if mouseClicked and clickX ~= nil then
|
||||
mx, my = clickX, clickY
|
||||
elseif padOn then
|
||||
mx, my = padX, padY
|
||||
end
|
||||
Kit.beginFrame(mx, my, mouseClicked, wheelY)
|
||||
mouseClicked = false
|
||||
clickX, clickY = nil, nil
|
||||
wheelY = 0
|
||||
-- Modal shield. Kit has no z-order, so the picker cannot simply be drawn
|
||||
-- last: the chrome and the panel underneath would take the same tap. The
|
||||
@@ -656,6 +736,7 @@ function App.draw()
|
||||
Kit.blockClicks = false
|
||||
SpeciesPicker.draw(S, Kit, width, height)
|
||||
Kit.endFrame()
|
||||
PadInput.draw()
|
||||
|
||||
-- Only now, with the whole frame painted, is it safe to drop the editor.
|
||||
if S._closeRequested then finishClose() end
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
-- Virtual pointer for the save editor on Switch / handhelds / any gamepad.
|
||||
-- Mirrors the launcher's RomImporter pad cursor (speeds, deadzone, dual-path
|
||||
-- raw gate) without sharing that module -- keeps RomImporter risk-free.
|
||||
--
|
||||
-- Stick / D-pad move; real mouse motion yields so desktop stays normal.
|
||||
-- Callers (App.lua) map A → click, B → close, shoulders → tabs, right stick
|
||||
-- → wheel notches.
|
||||
|
||||
local SafeArea = require("src.core.SafeArea")
|
||||
local GamepadMap = require("src.core.GamepadMap")
|
||||
|
||||
local PAD_DEAD = 0.28
|
||||
local PAD_SPEED = 560
|
||||
local PAD_DPAD_SPEED = 420
|
||||
-- Right stick → Kit wheel notches: ~2 notches/sec at full deflection so lists
|
||||
-- scroll at a usable pace without flooding one frame.
|
||||
local PAD_WHEEL_RATE = 2.0
|
||||
|
||||
local PadInput = {}
|
||||
|
||||
local cursor = { x = 0, y = 0 }
|
||||
local active = false
|
||||
local inited = false
|
||||
local axis = { leftx = 0, lefty = 0, righty = 0 }
|
||||
local dir = {}
|
||||
local rawHatDirs = {}
|
||||
local lastMouseX, lastMouseY
|
||||
local wheelAcc = 0
|
||||
|
||||
local function activate()
|
||||
if active then return end
|
||||
local ox, oy, w, h = SafeArea.rect()
|
||||
if not inited then
|
||||
cursor.x = ox + w * 0.5
|
||||
cursor.y = oy + h * 0.45
|
||||
inited = true
|
||||
end
|
||||
active = true
|
||||
end
|
||||
|
||||
function PadInput.reset()
|
||||
cursor.x, cursor.y = 0, 0
|
||||
active = false
|
||||
inited = false
|
||||
axis.leftx, axis.lefty, axis.righty = 0, 0, 0
|
||||
for k in pairs(dir) do dir[k] = nil end
|
||||
for k in pairs(rawHatDirs) do rawHatDirs[k] = nil end
|
||||
lastMouseX, lastMouseY = nil, nil
|
||||
wheelAcc = 0
|
||||
end
|
||||
|
||||
-- Touch / mouse press: drop the virtual cursor for this interaction so a tap
|
||||
-- is not swallowed by the Joy-Con pointer sitting elsewhere on screen.
|
||||
function PadInput.yieldToPointer()
|
||||
active = false
|
||||
end
|
||||
|
||||
-- Returns mx, my, isActive. When inactive the caller should use the system
|
||||
-- mouse; when active these coords feed Kit.beginFrame.
|
||||
function PadInput.pointer()
|
||||
return cursor.x, cursor.y, active
|
||||
end
|
||||
|
||||
function PadInput.isActive()
|
||||
return active
|
||||
end
|
||||
|
||||
-- Consume accumulated right-stick scroll as integer wheel notches (same
|
||||
-- units App.wheelmoved feeds Kit). Fractional remainder stays for next frame.
|
||||
function PadInput.takeWheel()
|
||||
local notches = 0
|
||||
if wheelAcc >= 1 or wheelAcc <= -1 then
|
||||
notches = wheelAcc > 0 and math.floor(wheelAcc) or math.ceil(wheelAcc)
|
||||
wheelAcc = wheelAcc - notches
|
||||
end
|
||||
return notches
|
||||
end
|
||||
|
||||
function PadInput.update(dt)
|
||||
if not (love and love.mouse and love.mouse.getPosition) then return end
|
||||
local mx, my = love.mouse.getPosition()
|
||||
if lastMouseX and active then
|
||||
if math.abs(mx - lastMouseX) > 3 or math.abs(my - lastMouseY) > 3 then
|
||||
active = false
|
||||
end
|
||||
end
|
||||
lastMouseX, lastMouseY = mx, my
|
||||
|
||||
local ax = axis.leftx or 0
|
||||
local ay = axis.lefty or 0
|
||||
local dx, dy = 0, 0
|
||||
if math.abs(ax) > PAD_DEAD then dx = dx + ax end
|
||||
if math.abs(ay) > PAD_DEAD then dy = dy + ay end
|
||||
if dir.dpleft then dx = dx - 1 end
|
||||
if dir.dpright then dx = dx + 1 end
|
||||
if dir.dpup then dy = dy - 1 end
|
||||
if dir.dpdown then dy = dy + 1 end
|
||||
|
||||
if dx ~= 0 or dy ~= 0 then
|
||||
activate()
|
||||
local mag = math.sqrt(dx * dx + dy * dy)
|
||||
if mag > 1 then dx, dy = dx / mag, dy / mag end
|
||||
local speed = (math.abs(ax) > PAD_DEAD or math.abs(ay) > PAD_DEAD)
|
||||
and PAD_SPEED or PAD_DPAD_SPEED
|
||||
local ox, oy, w, h = SafeArea.rect()
|
||||
local nx = cursor.x + dx * speed * dt
|
||||
local ny = cursor.y + dy * speed * dt
|
||||
cursor.x = math.max(ox, math.min(ox + w, nx))
|
||||
cursor.y = math.max(oy, math.min(oy + h, ny))
|
||||
end
|
||||
|
||||
local ry = axis.righty or 0
|
||||
if math.abs(ry) > PAD_DEAD then
|
||||
activate()
|
||||
-- Negative righty (stick up) scrolls lists up = positive wheel notches.
|
||||
wheelAcc = wheelAcc + (-ry) * PAD_WHEEL_RATE * dt
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns a string action the App layer handles:
|
||||
-- "a" | "b" | "tab_prev" | "tab_next" | nil
|
||||
function PadInput.gamepadpressed(_, button)
|
||||
activate()
|
||||
local action = GamepadMap.mapGamepadButton(button)
|
||||
if action == "a" or action == "b" then
|
||||
return action
|
||||
elseif button == "leftshoulder" then
|
||||
return "tab_prev"
|
||||
elseif button == "rightshoulder" then
|
||||
return "tab_next"
|
||||
elseif button == "dpup" or button == "dpdown"
|
||||
or button == "dpleft" or button == "dpright" then
|
||||
dir[button] = true
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function PadInput.gamepadreleased(_, button)
|
||||
if button == "dpup" or button == "dpdown"
|
||||
or button == "dpleft" or button == "dpright" then
|
||||
dir[button] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function PadInput.gamepadaxis(_, axisName, value)
|
||||
if axisName == "leftx" or axisName == "lefty" or axisName == "righty" then
|
||||
axis[axisName] = value
|
||||
if math.abs(value) > PAD_DEAD then activate() end
|
||||
end
|
||||
end
|
||||
|
||||
function PadInput.joystickpressed(joystick, button)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return nil end
|
||||
local padButton = GamepadMap.mapRawToGamepadButton(button)
|
||||
if padButton then return PadInput.gamepadpressed(joystick, padButton) end
|
||||
return nil
|
||||
end
|
||||
|
||||
function PadInput.joystickreleased(joystick, button)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return end
|
||||
local padButton = GamepadMap.mapRawToGamepadButton(button)
|
||||
if padButton then PadInput.gamepadreleased(joystick, padButton) end
|
||||
end
|
||||
|
||||
function PadInput.joystickaxis(joystick, axisIndex, value)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return end
|
||||
if axisIndex == 1 then
|
||||
PadInput.gamepadaxis(joystick, "leftx", value)
|
||||
elseif axisIndex == 2 then
|
||||
PadInput.gamepadaxis(joystick, "lefty", value)
|
||||
end
|
||||
end
|
||||
|
||||
function PadInput.joystickhat(joystick, hat, direction)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return end
|
||||
for _, d in ipairs(rawHatDirs[hat] or {}) do
|
||||
dir[d] = nil
|
||||
end
|
||||
local dirs = ({
|
||||
u = { "dpup" }, d = { "dpdown" }, l = { "dpleft" }, r = { "dpright" },
|
||||
lu = { "dpleft", "dpup" }, ru = { "dpright", "dpup" },
|
||||
ld = { "dpleft", "dpdown" }, rd = { "dpright", "dpdown" },
|
||||
})[direction] or {}
|
||||
for _, d in ipairs(dirs) do dir[d] = true end
|
||||
rawHatDirs[hat] = dirs
|
||||
if #dirs > 0 then activate() end
|
||||
end
|
||||
|
||||
function PadInput.draw()
|
||||
if not active then return end
|
||||
if not (love and love.graphics) then return end
|
||||
local x, y = cursor.x, cursor.y
|
||||
love.graphics.push("all")
|
||||
if love.graphics.origin then love.graphics.origin() end
|
||||
if love.graphics.setLineWidth then love.graphics.setLineWidth(1) end
|
||||
love.graphics.setColor(0, 0, 0, 0.45)
|
||||
if love.graphics.polygon then
|
||||
love.graphics.polygon("fill",
|
||||
x + 2, y + 2, x + 2, y + 22, x + 8, y + 16, x + 14, y + 26,
|
||||
x + 18, y + 24, x + 11, y + 14, x + 20, y + 14)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.polygon("fill",
|
||||
x, y, x, y + 20, x + 6, y + 14, x + 12, y + 24,
|
||||
x + 16, y + 22, x + 9, y + 12, x + 18, y + 12)
|
||||
love.graphics.setColor(0.05, 0.07, 0.12, 1)
|
||||
love.graphics.polygon("line",
|
||||
x, y, x, y + 20, x + 6, y + 14, x + 12, y + 24,
|
||||
x + 16, y + 22, x + 9, y + 12, x + 18, y + 12)
|
||||
else
|
||||
love.graphics.rectangle("fill", x, y, 12, 18)
|
||||
end
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
return PadInput
|
||||
@@ -37,6 +37,7 @@ If the file isn't there (or you want another copy), use **Open...**, drop a
|
||||
| --- | --- |
|
||||
| `Theme.lua` | the launcher's palette + drawing primitives (cards, glow, dashed outlines, letterspaced captions) |
|
||||
| `Kit.lua` | immediate-mode widgets built on Theme: buttons, rows, meters, chips, checkboxes, a real text field, pagers |
|
||||
| `PadInput.lua` | virtual cursor for Switch / gamepads (stick move, A click, B close, shoulders cycle tabs) |
|
||||
| `Ops.lua` | **every mutation**, behind one funnel that sets dirty + status together |
|
||||
| `App.lua` | chrome (version rail, title bar, tab rail, status bar) and the panel router |
|
||||
| `panels/` | one file per tab; pure layout that dispatches into Ops |
|
||||
@@ -46,6 +47,15 @@ the modal species search the inspector opens, drawn by `App.draw` after the
|
||||
panel rather than routed through the tab table. Kit has no z-order, so while
|
||||
it is up `Kit.blockClicks` shields every widget underneath it.
|
||||
|
||||
### Switch / gamepad
|
||||
|
||||
On Nintendo Switch (and any gamepad without a mouse), the editor uses the same
|
||||
virtual-cursor idea as the launcher: left stick / D-pad moves a pointer, **A**
|
||||
clicks, **B** closes (with the usual unsaved confirm), L/R cycle tabs, and the
|
||||
right stick scrolls lists. Touch taps forward as clicks. Without that path the
|
||||
editor soft-locked until HOME — `main.lua` used to drop all pad/touch events
|
||||
while `editorMode` was set.
|
||||
|
||||
The design reference is the `SaveEditor.dc.html` mockup that this port
|
||||
transcribes; its measurements are in the same pixel space `App.lua` draws in.
|
||||
|
||||
@@ -70,6 +80,7 @@ luajit tests/save_editor_task6_tests.lua # Boxes + Items rules
|
||||
luajit tests/save_editor_task7_tests.lua # Events + Dex rules
|
||||
luajit tests/save_editor_task8_tests.lua # map browser + spawn points
|
||||
luajit tests/save_editor_mod_tests.lua # modded species/items stay editable
|
||||
luajit tests/save_editor_pad_input_test.lua # pad cursor / NX input routing
|
||||
```
|
||||
|
||||
They drive `Ops.lua` rather than clicking pixel coordinates. The panels are
|
||||
|
||||
Reference in New Issue
Block a user