mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
on screen controller editing CLOSES #327
This commit is contained in:
@@ -271,6 +271,14 @@ the last controller restores it immediately. Layout re-derives from the
|
||||
window size on rotation. Desktop testing: `POKEPORT_TOUCH=1 love .` forces
|
||||
the overlay on and lets the mouse act as a finger (`=0` forces it off).
|
||||
|
||||
The launcher's **Touch Controls** button opens a drag editor: move each
|
||||
button freely, **Disable** to hide the overlay permanently (for
|
||||
controllers / emulation handhelds — distinct from the temporary
|
||||
gamepad auto-hide), **Reset** for defaults, **Done** to save into
|
||||
`options.lua` as normalized window fractions so rotation keeps the
|
||||
relative placement. In-game, Options → **TOUCH PAD** toggles the same
|
||||
on/off flag without leaving a play session.
|
||||
|
||||
## Translation support
|
||||
|
||||
Every string the player can read is now reachable from a mod, so a
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
||||
|
||||
local Game, EditorApp, Importer
|
||||
local Game, EditorApp, Importer, TouchEditor
|
||||
|
||||
local autopilot -- optional scripted-input dev tool (tests/autopilot.lua)
|
||||
local driverCo -- optional frame-driver (POKEPORT_DRIVER=file.lua): a
|
||||
@@ -134,6 +134,26 @@ function closeEditor()
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------ touch controls editor
|
||||
-- Suspends the launcher while the player drags on-screen buttons / toggles
|
||||
-- the overlay off (#327). No ROM cache needed -- options.lua only.
|
||||
local touchEditorHost
|
||||
local closeTouchControlsEditor -- forward declaration
|
||||
|
||||
local function openTouchControlsEditor()
|
||||
touchEditorHost = Importer
|
||||
Importer = nil
|
||||
TouchEditor = require("src.ui.TouchControlsEditor")
|
||||
TouchEditor.load({ onClose = function() closeTouchControlsEditor() end })
|
||||
end
|
||||
|
||||
function closeTouchControlsEditor()
|
||||
if TouchEditor and TouchEditor.unload then TouchEditor.unload() end
|
||||
TouchEditor = nil
|
||||
Importer = touchEditorHost
|
||||
touchEditorHost = nil
|
||||
end
|
||||
|
||||
local function bootGame(version)
|
||||
-- The launcher hands us the chosen game (Red / Blue / Yellow); scripted and
|
||||
-- headless runs fall back to POKEPORT_VERSION, then Red. Set the active
|
||||
@@ -239,11 +259,17 @@ function love.load(args)
|
||||
Importer = RomImporter.new(function(version)
|
||||
Importer = nil
|
||||
bootGame(version)
|
||||
end, { launcher = true, forceImport = forceImport, onEditSave = openEditor })
|
||||
end, {
|
||||
launcher = true,
|
||||
forceImport = forceImport,
|
||||
onEditSave = openEditor,
|
||||
onEditTouchControls = openTouchControlsEditor,
|
||||
})
|
||||
end
|
||||
|
||||
function love.update(dt)
|
||||
if editorMode then return EditorApp.update(dt) end
|
||||
if TouchEditor then return TouchEditor.update(dt) end
|
||||
if Importer then return Importer:update(dt) end
|
||||
|
||||
-- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly
|
||||
@@ -283,6 +309,7 @@ end
|
||||
|
||||
function love.draw()
|
||||
if editorMode then return EditorApp.draw() end
|
||||
if TouchEditor then return TouchEditor.draw() end
|
||||
if Importer then return Importer:draw() end
|
||||
|
||||
Game:draw()
|
||||
@@ -303,60 +330,61 @@ end
|
||||
|
||||
function love.keypressed(key, scancode, isrepeat)
|
||||
if editorMode then return EditorApp.keypressed(key) end
|
||||
if TouchEditor then return TouchEditor.keypressed(key) end
|
||||
if Importer then return Importer:keypressed(key) end
|
||||
Game:keypressed(key)
|
||||
end
|
||||
|
||||
function love.keyreleased(key)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return end
|
||||
Game:keyreleased(key)
|
||||
end
|
||||
|
||||
function love.gamepadpressed(joystick, button)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return Importer:gamepadpressed(joystick, button) end
|
||||
Game:gamepadpressed(joystick, button)
|
||||
end
|
||||
|
||||
function love.gamepadreleased(joystick, button)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return Importer:gamepadreleased(joystick, button) end
|
||||
Game:gamepadreleased(joystick, button)
|
||||
end
|
||||
|
||||
function love.gamepadaxis(joystick, axis, value)
|
||||
if editorMode then return end
|
||||
if editorMode or 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)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return Importer:joystickpressed(joystick, button) end
|
||||
Game:joystickpressed(joystick, button)
|
||||
end
|
||||
|
||||
function love.joystickreleased(joystick, button)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return Importer:joystickreleased(joystick, button) end
|
||||
Game:joystickreleased(joystick, button)
|
||||
end
|
||||
|
||||
function love.joystickaxis(joystick, axis, value)
|
||||
if editorMode then return end
|
||||
if editorMode or 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)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
||||
Game:joystickhat(joystick, hat, direction)
|
||||
end
|
||||
|
||||
function love.joystickremoved(joystick)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return end
|
||||
Game:joystickremoved(joystick)
|
||||
end
|
||||
@@ -365,7 +393,7 @@ end
|
||||
-- direction's key-up can be delivered to the OS instead of the game while
|
||||
-- unfocused, so reset input on either transition rather than trust it.
|
||||
function love.focus(f)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then
|
||||
if Importer.focus then Importer:focus(f) end
|
||||
return
|
||||
@@ -375,13 +403,19 @@ end
|
||||
|
||||
-- v is true when the window becomes visible again, false on minimize.
|
||||
function love.visible(v)
|
||||
if editorMode then return end
|
||||
if editorMode or TouchEditor then return end
|
||||
if Importer then return end
|
||||
Game:visible(v)
|
||||
end
|
||||
|
||||
function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
if editorMode then return end
|
||||
if TouchEditor then
|
||||
-- iOS synthesizes mousepressed for the primary touch (same as the
|
||||
-- launcher); Android drives the editor through love.touch directly.
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
return TouchEditor.touchpressed(id, x, y)
|
||||
end
|
||||
if Importer then
|
||||
-- iOS: LÖVE already synthesizes a mousepressed for the primary touch,
|
||||
-- and love.mousepressed below forwards that to the Importer, so
|
||||
@@ -399,12 +433,20 @@ end
|
||||
|
||||
function love.touchmoved(id, x, y, dx, dy, pressure)
|
||||
if editorMode then return end
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
return TouchEditor.touchmoved(id, x, y)
|
||||
end
|
||||
if Importer then return end
|
||||
Game:touchmoved(id, x, y)
|
||||
end
|
||||
|
||||
function love.touchreleased(id, x, y, dx, dy, pressure)
|
||||
if editorMode then return end
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
return TouchEditor.touchreleased(id, x, y)
|
||||
end
|
||||
if Importer then return end
|
||||
Game:touchreleased(id, x, y)
|
||||
end
|
||||
@@ -414,11 +456,18 @@ function love.wheelmoved(x, y)
|
||||
if EditorApp.wheelmoved then return EditorApp.wheelmoved(x, y) end
|
||||
return
|
||||
end
|
||||
if TouchEditor then return end
|
||||
if Importer then return end
|
||||
Game:wheelmoved(x, y)
|
||||
end
|
||||
|
||||
function love.mousepressed(x, y, button)
|
||||
if TouchEditor then
|
||||
-- Android primary touch already arrived via love.touchpressed; a second
|
||||
-- mouse path would double-fire Done / begin a second drag.
|
||||
if love.system.getOS() == "Android" then return end
|
||||
return TouchEditor.mousepressed(x, y, button)
|
||||
end
|
||||
if Importer then return Importer:mousepressed(x, y, button) end
|
||||
if editorMode and EditorApp.mousepressed then
|
||||
return EditorApp.mousepressed(x, y, button)
|
||||
@@ -429,6 +478,10 @@ function love.mousepressed(x, y, button)
|
||||
end
|
||||
|
||||
function love.mousereleased(x, y, button)
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "Android" then return end
|
||||
return TouchEditor.mousereleased(x, y, button)
|
||||
end
|
||||
if Importer then return end
|
||||
if editorMode and EditorApp.mousereleased then
|
||||
return EditorApp.mousereleased(x, y, button)
|
||||
@@ -439,6 +492,10 @@ function love.mousereleased(x, y, button)
|
||||
end
|
||||
|
||||
function love.mousemoved(x, y)
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "Android" then return end
|
||||
return TouchEditor.mousemoved(x, y)
|
||||
end
|
||||
if editorMode or Importer then return end
|
||||
if mouseTouch and Game and love.mouse.isDown(1) then
|
||||
Game:touchmoved("mouse", x, y)
|
||||
@@ -446,6 +503,7 @@ function love.mousemoved(x, y)
|
||||
end
|
||||
|
||||
function love.textinput(text)
|
||||
if TouchEditor then return end
|
||||
if Importer then return Importer:textinput(text) end
|
||||
if editorMode and EditorApp.textinput then
|
||||
return EditorApp.textinput(text)
|
||||
|
||||
@@ -619,6 +619,7 @@ function Game:applyOptions(opts)
|
||||
-- fpsCap key pace at the standard rate (issue #88)
|
||||
require("src.core.FrameCap").applyOptions(opts)
|
||||
Input:applyBindings(opts.bindings)
|
||||
TouchControls:applyOptions(opts)
|
||||
-- heal soft-bricked APK installs that already saved gbcfx > 0 (#136)
|
||||
if gbcCleared then self:writeOptions() end
|
||||
end
|
||||
|
||||
@@ -249,6 +249,11 @@ function SaveData.defaultOptions()
|
||||
-- Native mod enablement is an installation option, not save-slot data.
|
||||
-- Missing entries mean enabled so newly installed mods work by default.
|
||||
mods = {},
|
||||
-- On-screen touch overlay (Android/iOS; see src/core/TouchControls.lua).
|
||||
-- enabled=false hides it permanently (distinct from auto-hide-on-gamepad).
|
||||
-- positions are optional normalized centers {x=0..1, y=0..1} per control
|
||||
-- (dpad/a/b/start/select); nil means the default layout.
|
||||
touchControls = { enabled = true },
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
+195
-48
@@ -12,6 +12,11 @@
|
||||
-- (main.lua then drives it with the mouse); POKEPORT_TOUCH=0 forces it
|
||||
-- off everywhere.
|
||||
--
|
||||
-- Player preferences (options.touchControls) can permanently disable the
|
||||
-- overlay and/or override per-control positions as normalized window
|
||||
-- fractions. The launcher editor (src/ui/TouchControlsEditor.lua) writes
|
||||
-- those; applyOptions reads them at boot and whenever options change.
|
||||
--
|
||||
-- Controls press GB buttons through Input:overlayPressed/Released -- their
|
||||
-- own input source, not a keyboard alias -- so a held overlay direction
|
||||
-- merges cleanly with a keyboard key or stick holding the same button,
|
||||
@@ -39,6 +44,7 @@ local DPAD_DEAD = 0.16
|
||||
local SLOP = { a = 1.3, b = 1.3, start = 1.4, select = 1.4 }
|
||||
|
||||
local BUTTONS = { "a", "b", "start", "select" }
|
||||
local CONTROLS = { "dpad", "a", "b", "start", "select" }
|
||||
|
||||
local IMAGES = {
|
||||
dpad = "assets/touch/dpad.png",
|
||||
@@ -52,6 +58,12 @@ local IMAGES = {
|
||||
select = "assets/touch/select.png",
|
||||
}
|
||||
|
||||
local function clamp01(v)
|
||||
if v < 0 then return 0 end
|
||||
if v > 1 then return 1 end
|
||||
return v
|
||||
end
|
||||
|
||||
local function wantsOverlay()
|
||||
local env = os.getenv("POKEPORT_TOUCH")
|
||||
if env == "1" then return true end
|
||||
@@ -60,8 +72,59 @@ local function wantsOverlay()
|
||||
return osName == "Android" or osName == "iOS"
|
||||
end
|
||||
|
||||
-- Normalize a persisted touchControls table into {enabled, positions}.
|
||||
-- Unknown / garbage keys are dropped so a bad options.lua cannot brick
|
||||
-- the overlay.
|
||||
function TouchControls.normalizeConfig(tc)
|
||||
local out = { enabled = true, positions = nil }
|
||||
if type(tc) ~= "table" then return out end
|
||||
if tc.enabled == false then out.enabled = false end
|
||||
if type(tc.positions) == "table" then
|
||||
local pos = {}
|
||||
for _, name in ipairs(CONTROLS) do
|
||||
local p = tc.positions[name]
|
||||
if type(p) == "table" and type(p.x) == "number" and type(p.y) == "number" then
|
||||
pos[name] = { x = clamp01(p.x), y = clamp01(p.y) }
|
||||
end
|
||||
end
|
||||
if next(pos) then out.positions = pos end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Pure default layout in LOVE units for a given window size. Shared by
|
||||
-- layout() and the editor's Reset path so defaults stay in one place.
|
||||
function TouchControls.defaultLayout(ww, wh)
|
||||
local short = math.min(ww, wh)
|
||||
local dpadW = math.min(180, short * 0.34)
|
||||
local abW = dpadW * 0.46
|
||||
local ssW = dpadW * 0.30
|
||||
local margin = dpadW * 0.12
|
||||
return {
|
||||
dpad = { cx = margin + dpadW / 2, cy = wh - margin - dpadW / 2, w = dpadW },
|
||||
a = { cx = ww - margin - abW * 0.55, cy = wh - margin - abW * 1.75, w = abW },
|
||||
b = { cx = ww - margin - abW * 1.60, cy = wh - margin - abW * 0.55, w = abW },
|
||||
start = { cx = ww / 2 + ssW * 0.60, cy = wh - margin - ssW * 0.95, w = ssW },
|
||||
select = { cx = ww / 2 - ssW * 0.60, cy = wh - margin - ssW * 0.95, w = ssW },
|
||||
}
|
||||
end
|
||||
|
||||
local function loadImages()
|
||||
local img = {}
|
||||
for name, path in pairs(IMAGES) do
|
||||
local ok, im = pcall(love.graphics.newImage, path)
|
||||
if not ok then return nil end
|
||||
im:setFilter("linear", "linear")
|
||||
img[name] = im
|
||||
end
|
||||
return img
|
||||
end
|
||||
|
||||
function TouchControls:init()
|
||||
self.active = wantsOverlay()
|
||||
self.enabled = true
|
||||
self.positions = nil
|
||||
self.preview = false
|
||||
self.controllerHidden = false
|
||||
self.touches = {}
|
||||
-- per-GB-button owner count: two fingers on A must not double-press it,
|
||||
@@ -70,51 +133,86 @@ function TouchControls:init()
|
||||
self.dpadTouch = nil
|
||||
self.layoutW, self.layoutH = nil, nil
|
||||
self.img = nil
|
||||
if not self.active then return end
|
||||
-- soft-fail: a missing/corrupt PNG must never block boot; the overlay
|
||||
-- stays off and keyboard/controller play still works
|
||||
local img = {}
|
||||
for name, path in pairs(IMAGES) do
|
||||
local ok, im = pcall(love.graphics.newImage, path)
|
||||
if not ok then
|
||||
img = nil
|
||||
break
|
||||
end
|
||||
-- smooth UI icons; the global default filter is nearest for GB pixels
|
||||
im:setFilter("linear", "linear")
|
||||
img[name] = im
|
||||
-- Images load whenever the platform wants the overlay OR the launcher
|
||||
-- editor forces a preview (desktop testing of the editor).
|
||||
if self.active then
|
||||
self.img = loadImages()
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure art is loaded for the launcher editor even when wantsOverlay()
|
||||
-- is false (desktop without POKEPORT_TOUCH).
|
||||
function TouchControls:ensureImages()
|
||||
if self.img then return true end
|
||||
self.img = loadImages()
|
||||
return self.img ~= nil
|
||||
end
|
||||
|
||||
-- Apply options.touchControls. Called from Game:applyOptions and from
|
||||
-- the launcher editor after a save.
|
||||
function TouchControls:applyOptions(opts)
|
||||
local cfg = TouchControls.normalizeConfig(opts and opts.touchControls)
|
||||
self.enabled = cfg.enabled
|
||||
self.positions = cfg.positions
|
||||
self.layoutW, self.layoutH = nil, nil
|
||||
if not self.enabled then
|
||||
self.controllerHidden = false
|
||||
self:reset()
|
||||
end
|
||||
end
|
||||
|
||||
function TouchControls:config()
|
||||
return {
|
||||
enabled = self.enabled ~= false,
|
||||
positions = self.positions,
|
||||
}
|
||||
end
|
||||
|
||||
-- Preview mode: force-draw the overlay for the layout editor, ignoring
|
||||
-- platform / enabled / gamepad gates. Gameplay input still respects
|
||||
-- enabled via touchpressed.
|
||||
function TouchControls:setPreview(on)
|
||||
self.preview = on and true or false
|
||||
if on then
|
||||
self:ensureImages()
|
||||
self.controllerHidden = false
|
||||
end
|
||||
self.img = img
|
||||
end
|
||||
|
||||
function TouchControls:visible()
|
||||
return self.active and self.img ~= nil and not self.controllerHidden
|
||||
if self.preview then return self.img ~= nil end
|
||||
return self.active and self.enabled ~= false and self.img ~= nil
|
||||
and not self.controllerHidden
|
||||
end
|
||||
|
||||
local function clampZone(zone, ww, wh)
|
||||
local half = zone.w * 0.5
|
||||
zone.cx = math.max(half, math.min(ww - half, zone.cx))
|
||||
zone.cy = math.max(half, math.min(wh - half, zone.cy))
|
||||
end
|
||||
|
||||
-- Layout in LOVE units (density-independent on mobile), recomputed when
|
||||
-- the window size changes (rotation, resize). D-pad bottom-left, B/A
|
||||
-- bottom-right with A above B (the Game Boy diagonal), START/SELECT
|
||||
-- flanking the bottom center.
|
||||
-- the window size changes (rotation, resize). Default: d-pad bottom-left,
|
||||
-- B/A bottom-right with A above B (the Game Boy diagonal), START/SELECT
|
||||
-- flanking the bottom center. Custom positions (normalized 0..1) override
|
||||
-- centers while sizes stay derived from the short edge.
|
||||
function TouchControls:layout()
|
||||
local ww, wh = love.graphics.getDimensions()
|
||||
if self.layoutW == ww and self.layoutH == wh then return self.L end
|
||||
if self.layoutW == ww and self.layoutH == wh and self.L then return self.L end
|
||||
self.layoutW, self.layoutH = ww, wh
|
||||
local short = math.min(ww, wh)
|
||||
-- ~a third of the short edge, capped so tablets don't get a dinner plate
|
||||
local dpadW = math.min(180, short * 0.34)
|
||||
local abW = dpadW * 0.46
|
||||
local ssW = dpadW * 0.30
|
||||
local margin = dpadW * 0.12
|
||||
-- START/SELECT hug the bottom center: on a narrow portrait phone the
|
||||
-- d-pad and B leave little room, so a tight pair is what keeps them off
|
||||
-- the neighboring controls
|
||||
self.L = {
|
||||
dpad = { cx = margin + dpadW / 2, cy = wh - margin - dpadW / 2, w = dpadW },
|
||||
a = { cx = ww - margin - abW * 0.55, cy = wh - margin - abW * 1.75, w = abW },
|
||||
b = { cx = ww - margin - abW * 1.60, cy = wh - margin - abW * 0.55, w = abW },
|
||||
start = { cx = ww / 2 + ssW * 0.60, cy = wh - margin - ssW * 0.95, w = ssW },
|
||||
select = { cx = ww / 2 - ssW * 0.60, cy = wh - margin - ssW * 0.95, w = ssW },
|
||||
}
|
||||
self.L = TouchControls.defaultLayout(ww, wh)
|
||||
if self.positions then
|
||||
for _, name in ipairs(CONTROLS) do
|
||||
local p = self.positions[name]
|
||||
local zone = self.L[name]
|
||||
if p and zone then
|
||||
zone.cx = p.x * ww
|
||||
zone.cy = p.y * wh
|
||||
clampZone(zone, ww, wh)
|
||||
end
|
||||
end
|
||||
end
|
||||
local ssW = self.L.start.w
|
||||
local fontSize = math.max(8, math.floor(ssW * 0.26))
|
||||
if not self.labelFont or self.fontSize ~= fontSize then
|
||||
self.fontSize = fontSize
|
||||
@@ -123,12 +221,45 @@ function TouchControls:layout()
|
||||
return self.L
|
||||
end
|
||||
|
||||
-- Move one control to a screen-space point and persist its normalized
|
||||
-- position. Used by the layout editor while dragging.
|
||||
function TouchControls:setControlCenter(name, cx, cy)
|
||||
local ww, wh = love.graphics.getDimensions()
|
||||
local L = self:layout()
|
||||
local zone = L[name]
|
||||
if not zone then return end
|
||||
zone.cx, zone.cy = cx, cy
|
||||
clampZone(zone, ww, wh)
|
||||
self.positions = self.positions or {}
|
||||
self.positions[name] = { x = zone.cx / ww, y = zone.cy / wh }
|
||||
end
|
||||
|
||||
function TouchControls:clearPositions()
|
||||
self.positions = nil
|
||||
self.layoutW, self.layoutH = nil, nil
|
||||
end
|
||||
|
||||
local function inCircle(zone, x, y, slop)
|
||||
local r = zone.w * 0.5 * slop
|
||||
local dx, dy = x - zone.cx, y - zone.cy
|
||||
return dx * dx + dy * dy <= r * r
|
||||
end
|
||||
|
||||
-- Which control (if any) contains (x, y). Prefer face buttons over the
|
||||
-- d-pad when they overlap, matching touchpressed's order.
|
||||
function TouchControls:hitTest(x, y)
|
||||
local L = self:layout()
|
||||
for _, btn in ipairs(BUTTONS) do
|
||||
if inCircle(L[btn], x, y, SLOP[btn]) then return btn end
|
||||
end
|
||||
local dz = L.dpad
|
||||
local half = dz.w * 0.65
|
||||
if math.abs(x - dz.cx) <= half and math.abs(y - dz.cy) <= half then
|
||||
return "dpad"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function dpadDir(zone, x, y)
|
||||
local dx, dy = x - zone.cx, y - zone.cy
|
||||
local dead = zone.w * DPAD_DEAD
|
||||
@@ -165,7 +296,9 @@ local function setDpad(self, touch, dir)
|
||||
end
|
||||
|
||||
function TouchControls:touchpressed(id, x, y)
|
||||
if not (self.active and self.img) then return end
|
||||
-- preview mode is layout-edit only: never press GB buttons
|
||||
if self.preview then return end
|
||||
if not (self.active and self.enabled ~= false and self.img) then return end
|
||||
-- a controller hid the overlay; the first touch only brings it back
|
||||
if self.controllerHidden then
|
||||
self.controllerHidden = false
|
||||
@@ -192,6 +325,7 @@ function TouchControls:touchpressed(id, x, y)
|
||||
end
|
||||
|
||||
function TouchControls:touchmoved(id, x, y)
|
||||
if self.preview then return end
|
||||
local touch = self.touches[id]
|
||||
-- only the d-pad tracks movement (slide between directions without
|
||||
-- lifting); buttons hold until release wherever the finger wanders
|
||||
@@ -200,6 +334,7 @@ function TouchControls:touchmoved(id, x, y)
|
||||
end
|
||||
|
||||
function TouchControls:touchreleased(id, x, y)
|
||||
if self.preview then return end
|
||||
local touch = self.touches[id]
|
||||
if not touch then return end
|
||||
self.touches[id] = nil
|
||||
@@ -216,7 +351,7 @@ end
|
||||
-- touchreleased and would strand its button held forever. Called from
|
||||
-- Game alongside Input:reset() on focus/visibility loss.
|
||||
function TouchControls:reset()
|
||||
for btn in pairs(self.held) do
|
||||
for btn in pairs(self.held or {}) do
|
||||
Input:overlayReleased(btn)
|
||||
end
|
||||
self.held = {}
|
||||
@@ -225,9 +360,13 @@ function TouchControls:reset()
|
||||
end
|
||||
|
||||
-- a gamepad is being used: hide the overlay (dropping anything it held)
|
||||
-- until the next screen touch asks for it back
|
||||
-- until the next screen touch asks for it back. No-op when the player
|
||||
-- permanently disabled the overlay -- there is nothing to hide, and a
|
||||
-- later accidental touch must not resurrect it.
|
||||
function TouchControls:noteGamepad()
|
||||
if not self.active or self.controllerHidden then return end
|
||||
if not self.active or self.enabled == false or self.controllerHidden then
|
||||
return
|
||||
end
|
||||
self.controllerHidden = true
|
||||
self:reset()
|
||||
end
|
||||
@@ -242,40 +381,46 @@ function TouchControls:joystickremoved()
|
||||
end
|
||||
end
|
||||
|
||||
local function drawIcon(img, zone, pressed)
|
||||
love.graphics.setColor(1, 1, 1, pressed and BACK_PRESSED or BACK)
|
||||
local function drawIcon(img, zone, pressed, alphaMul)
|
||||
alphaMul = alphaMul or 1
|
||||
love.graphics.setColor(1, 1, 1, (pressed and BACK_PRESSED or BACK) * alphaMul)
|
||||
love.graphics.circle("fill", zone.cx, zone.cy, zone.w * 0.58)
|
||||
local scale = zone.w / img:getWidth()
|
||||
love.graphics.setColor(1, 1, 1, pressed and ALPHA_PRESSED or ALPHA)
|
||||
love.graphics.setColor(1, 1, 1, (pressed and ALPHA_PRESSED or ALPHA) * alphaMul)
|
||||
love.graphics.draw(img, zone.cx - zone.w / 2,
|
||||
zone.cy - img:getHeight() * scale / 2, 0, scale, scale)
|
||||
end
|
||||
|
||||
-- Screen-space, called by Game:draw after Renderer:endFrame so the
|
||||
-- overlay rides on top of everything (world, UI, CRT/GBC FX included).
|
||||
-- Also used by the launcher layout editor under preview mode.
|
||||
function TouchControls:draw()
|
||||
if not self:visible() then return end
|
||||
local L = self:layout()
|
||||
-- when the player disabled the overlay but the editor is previewing,
|
||||
-- draw dimmed so the layout is still editable
|
||||
local alphaMul = (self.preview and self.enabled == false) and 0.45 or 1
|
||||
love.graphics.push("all")
|
||||
love.graphics.origin()
|
||||
|
||||
local dpadTouch = self.dpadTouch and self.touches[self.dpadTouch]
|
||||
local dir = dpadTouch and dpadTouch.dir
|
||||
drawIcon(dir and self.img["dpad_" .. dir] or self.img.dpad, L.dpad,
|
||||
dir ~= nil)
|
||||
dir ~= nil, alphaMul)
|
||||
for _, btn in ipairs(BUTTONS) do
|
||||
drawIcon(self.img[btn], L[btn], self.held[btn] ~= nil)
|
||||
drawIcon(self.img[btn], L[btn], self.held[btn] ~= nil, alphaMul)
|
||||
end
|
||||
|
||||
-- the +/- glyphs alone don't say which is which; shadowed so the text
|
||||
-- reads on both the black letterbox and battle's white one
|
||||
-- reads on both the black letterbox and battle's white one. Each label
|
||||
-- tracks its own control's cy/w so dragging START cannot move SELECT.
|
||||
love.graphics.setFont(self.labelFont)
|
||||
local ly = L.start.cy + L.start.w * 0.66
|
||||
local function label(text, zone)
|
||||
local ly = zone.cy + zone.w * 0.66
|
||||
local w = self.labelFont:getWidth(text)
|
||||
love.graphics.setColor(0, 0, 0, 0.6)
|
||||
love.graphics.setColor(0, 0, 0, 0.6 * alphaMul)
|
||||
love.graphics.print(text, zone.cx - w / 2 + 1, ly + 1)
|
||||
love.graphics.setColor(1, 1, 1, ALPHA + 0.2)
|
||||
love.graphics.setColor(1, 1, 1, (ALPHA + 0.2) * alphaMul)
|
||||
love.graphics.print(text, zone.cx - w / 2, ly)
|
||||
end
|
||||
label("START", L.start)
|
||||
@@ -284,4 +429,6 @@ function TouchControls:draw()
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
TouchControls.CONTROLS = CONTROLS
|
||||
|
||||
return TouchControls
|
||||
|
||||
@@ -533,7 +533,9 @@ end
|
||||
-- forceImport (treat every version as not-yet-imported, so re-import is forced),
|
||||
-- onEditSave(version, slotId) (host handler for the Edit affordance on a save
|
||||
-- row -- main.lua opens the bundled save editor on that slot; when it is not
|
||||
-- supplied the Edit label is not drawn at all).
|
||||
-- supplied the Edit label is not drawn at all),
|
||||
-- onEditTouchControls() (host handler for the Touch Controls button -- main.lua
|
||||
-- opens the layout editor; when it is not supplied the button is not drawn).
|
||||
function RomImporter.new(onComplete, opts)
|
||||
opts = opts or {}
|
||||
-- iOS rides the same mobile import flows as Android: the save-dir
|
||||
@@ -548,6 +550,7 @@ function RomImporter.new(onComplete, opts)
|
||||
launcher = opts.launcher or false,
|
||||
forceImport = opts.forceImport or false,
|
||||
onEditSave = opts.onEditSave,
|
||||
onEditTouchControls = opts.onEditTouchControls,
|
||||
android = android,
|
||||
ios = mobileOS == "iOS",
|
||||
-- One startup poll pass: files dropped through the Files app are swept
|
||||
@@ -1622,6 +1625,9 @@ function RomImporter:_resetFrameRects()
|
||||
self.saveImportRect = nil
|
||||
self.saveExportRect = nil
|
||||
self.saveFolderRect = nil
|
||||
-- Rebuilt only by the active game panel; nil elsewhere so the mods tab
|
||||
-- cannot inherit last frame's Touch Controls button.
|
||||
self.touchControlsRect = nil
|
||||
end
|
||||
|
||||
function RomImporter:draw()
|
||||
@@ -2217,6 +2223,10 @@ function RomImporter:mousepressed(x, y, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if inside(self.touchControlsRect, x, y) then
|
||||
if self.onEditTouchControls then self.onEditTouchControls() end
|
||||
return
|
||||
end
|
||||
-- SAVE SLOT rows / Edit / Delete. The two labels are checked first so a tap
|
||||
-- on either never also selects the row. A press only ARMS a row click:
|
||||
-- _updateSlotDrag commits it on release when the pointer did not move (a
|
||||
@@ -2626,16 +2636,26 @@ function RomImporter:_drawGamePanel(version, x, y, w, h, paged)
|
||||
local sfFolderH = (sfNotice and sfNotice.dir) and (self.hintFont:getHeight() + 4 * s) or 0
|
||||
local saveFilesH = pad + labelH + 10 * s + sfBtnH + 6 * s + sfHintH + sfFolderH + pad
|
||||
local playH = math.max(50 * s, self.playFont:getHeight() + 30 * s)
|
||||
-- Touch Controls editor entry (layout + permanent disable). Drawn whenever
|
||||
-- the host supplied onEditTouchControls; height reserved only then so a
|
||||
-- scripted/headless importer without the callback stays compact.
|
||||
local touchBtnH = self.onEditTouchControls
|
||||
and math.max(38 * s, self.saveBtnFont:getHeight() + 20 * s) or 0
|
||||
local touchGap = self.onEditTouchControls and (12 * s) or 0
|
||||
|
||||
-- vertical placement of the left column
|
||||
local romY = bodyTop
|
||||
local saveFilesY = romY + romCardH + 12 * s
|
||||
local leftNaturalH = romCardH + 12 * s + saveFilesH + 12 * s + playH
|
||||
local playY
|
||||
local leftNaturalH = romCardH + 12 * s + saveFilesH + touchGap + touchBtnH
|
||||
+ 12 * s + playH
|
||||
local playY, touchY
|
||||
if twoCol and not paged then
|
||||
playY = bodyTop + bodyH - playH -- pinned to the column's bottom
|
||||
-- Play pinned to the column bottom; Touch Controls sits just above it
|
||||
playY = bodyTop + bodyH - playH
|
||||
touchY = playY - touchGap - touchBtnH
|
||||
else
|
||||
playY = saveFilesY + saveFilesH + 12 * s
|
||||
touchY = saveFilesY + saveFilesH + touchGap
|
||||
playY = touchY + touchBtnH + 12 * s
|
||||
end
|
||||
|
||||
-- ROM card
|
||||
@@ -2704,6 +2724,12 @@ function RomImporter:_drawGamePanel(version, x, y, w, h, paged)
|
||||
self.saveFolderRect = frect
|
||||
end
|
||||
|
||||
-- Touch Controls: open the drag-to-reposition / disable editor (#327).
|
||||
if self.onEditTouchControls and touchBtnH > 0 then
|
||||
self.touchControlsRect = self:_glassyButton(
|
||||
leftX, touchY, colW, touchBtnH, "Touch Controls", self.saveBtnFont, true)
|
||||
end
|
||||
|
||||
-- Play button
|
||||
self:_playButton(leftX, playY, colW, playH, gameName, ready, locked)
|
||||
|
||||
|
||||
@@ -322,6 +322,25 @@ local function buildRows(game)
|
||||
activate = function(g)
|
||||
require("src.ui.Screens").push(g, "BindingsMenu")
|
||||
end },
|
||||
-- permanent on-screen pad toggle (#327); layout editing stays in the
|
||||
-- launcher. Hidden where the overlay never appears (desktop without
|
||||
-- POKEPORT_TOUCH), so the row costs a non-mobile install nothing.
|
||||
{ id = "touchControls", label = Strings("TOUCH PAD"),
|
||||
value = function(g)
|
||||
local tc = g.save.options.touchControls
|
||||
local on = not (type(tc) == "table" and tc.enabled == false)
|
||||
return on and Strings("ON") or Strings("OFF")
|
||||
end,
|
||||
step = function(g)
|
||||
local o = g.save.options
|
||||
local tc = type(o.touchControls) == "table" and o.touchControls or {}
|
||||
local on = not (tc.enabled == false)
|
||||
tc.enabled = not on
|
||||
-- keep any saved positions when toggling
|
||||
o.touchControls = tc
|
||||
require("src.core.TouchControls"):applyOptions(o)
|
||||
return true
|
||||
end },
|
||||
}
|
||||
-- issue #136: hide GBC FX on Android/iOS -- the present shader soft-bricks
|
||||
if not GBCFX.isSupported() then
|
||||
@@ -331,6 +350,21 @@ local function buildRows(game)
|
||||
end
|
||||
rows = filtered
|
||||
end
|
||||
-- TOUCH PAD only where the overlay can appear (mobile, or desktop with
|
||||
-- POKEPORT_TOUCH=1). POKEPORT_TOUCH=0 forces it off everywhere.
|
||||
do
|
||||
local env = os.getenv("POKEPORT_TOUCH")
|
||||
local osName = love.system and love.system.getOS and love.system.getOS()
|
||||
local show = env == "1"
|
||||
or (env ~= "0" and (osName == "Android" or osName == "iOS"))
|
||||
if not show then
|
||||
local filtered = {}
|
||||
for _, row in ipairs(rows) do
|
||||
if row.id ~= "touchControls" then filtered[#filtered + 1] = row end
|
||||
end
|
||||
rows = filtered
|
||||
end
|
||||
end
|
||||
-- PIKACHU VOL only means something where the voice clips exist: Yellow
|
||||
-- (data.audio.pikaCries is the clip count the importer wrote). Red/Blue
|
||||
-- keep the row list they always had.
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
-- Launcher touch-controls editor (#327): drag each on-screen button to a
|
||||
-- new spot, toggle the overlay off entirely, Reset to defaults, Done to
|
||||
-- persist into options.lua. Opened from the game panel's "Touch Controls"
|
||||
-- button; main.lua suspends the launcher the same way it does for the
|
||||
-- save editor.
|
||||
--
|
||||
-- Draws in full window LOVE units -- the same space TouchControls uses
|
||||
-- after Renderer:endFrame -- so what you drag here is what you get in play.
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
|
||||
local Editor = {}
|
||||
|
||||
local PAL = {
|
||||
bgTop = { 8, 14, 36 },
|
||||
bgBot = { 4, 8, 22 },
|
||||
white = { 245, 248, 255 },
|
||||
label = { 160, 175, 210 },
|
||||
green = { 80, 220, 140 },
|
||||
red = { 240, 90, 110 },
|
||||
card = { 18, 28, 58 },
|
||||
stroke = { 120, 150, 220 },
|
||||
}
|
||||
|
||||
local function col(c, a)
|
||||
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, a or 1)
|
||||
end
|
||||
|
||||
local function inside(r, x, y)
|
||||
return r and x >= r.x and x <= r.x + r.w and y >= r.y and y <= r.y + r.h
|
||||
end
|
||||
|
||||
local function roundRect(mode, x, y, w, h, r)
|
||||
love.graphics.rectangle(mode, x, y, w, h, r, r)
|
||||
end
|
||||
|
||||
function Editor.load(opts)
|
||||
opts = opts or {}
|
||||
Editor.onClose = opts.onClose
|
||||
Editor.drag = nil
|
||||
Editor.rects = {}
|
||||
Editor.fonts = {
|
||||
title = love.graphics.newFont(28),
|
||||
body = love.graphics.newFont(16),
|
||||
btn = love.graphics.newFont(18),
|
||||
}
|
||||
local optsTbl = SaveData.loadOptions()
|
||||
TouchControls:init()
|
||||
TouchControls:ensureImages()
|
||||
TouchControls:applyOptions(optsTbl)
|
||||
TouchControls:setPreview(true)
|
||||
Editor.enabled = TouchControls.enabled ~= false
|
||||
end
|
||||
|
||||
function Editor.unload()
|
||||
TouchControls:setPreview(false)
|
||||
TouchControls:reset()
|
||||
Editor.drag = nil
|
||||
Editor.onClose = nil
|
||||
end
|
||||
|
||||
local function persist()
|
||||
local opts = SaveData.loadOptions()
|
||||
local cfg = TouchControls:config()
|
||||
opts.touchControls = {
|
||||
enabled = cfg.enabled,
|
||||
positions = cfg.positions,
|
||||
}
|
||||
SaveData.saveOptions(opts)
|
||||
end
|
||||
|
||||
local function close()
|
||||
persist()
|
||||
local cb = Editor.onClose
|
||||
Editor.unload()
|
||||
if cb then cb() end
|
||||
end
|
||||
|
||||
local function resetLayout()
|
||||
TouchControls:clearPositions()
|
||||
end
|
||||
|
||||
local function toggleEnabled()
|
||||
Editor.enabled = not Editor.enabled
|
||||
TouchControls.enabled = Editor.enabled
|
||||
if not Editor.enabled then TouchControls:reset() end
|
||||
end
|
||||
|
||||
function Editor.update(_dt)
|
||||
-- drag follows the live pointer when love.touch / mouse is available;
|
||||
-- touchmoved / mousemoved also update, so this is a belt-and-suspenders
|
||||
-- path for Android where move events can be thin
|
||||
if not Editor.drag then return end
|
||||
local x, y
|
||||
if love.touch and love.touch.getPosition and Editor.drag.touchId then
|
||||
local ok, tx, ty = pcall(love.touch.getPosition, Editor.drag.touchId)
|
||||
if ok and tx then x, y = tx, ty end
|
||||
end
|
||||
if not x and love.mouse then
|
||||
x, y = love.mouse.getPosition()
|
||||
end
|
||||
if x then
|
||||
TouchControls:setControlCenter(
|
||||
Editor.drag.name,
|
||||
x - Editor.drag.offX,
|
||||
y - Editor.drag.offY)
|
||||
end
|
||||
end
|
||||
|
||||
function Editor.draw()
|
||||
local ww, wh = love.graphics.getDimensions()
|
||||
local s = math.max(0.75, math.min(1.4, wh / 768))
|
||||
Editor.rects = {}
|
||||
|
||||
-- radial-ish navy field (two stacked fills; matches launcher atmosphere)
|
||||
col(PAL.bgBot)
|
||||
love.graphics.rectangle("fill", 0, 0, ww, wh)
|
||||
col(PAL.bgTop, 0.85)
|
||||
love.graphics.circle("fill", ww * 0.5, wh * 0.15, math.max(ww, wh) * 0.55)
|
||||
|
||||
local pad = 18 * s
|
||||
local barH = 56 * s
|
||||
local btnH = 40 * s
|
||||
local btnW = 100 * s
|
||||
|
||||
-- top bar
|
||||
col(PAL.card, 0.92)
|
||||
love.graphics.rectangle("fill", 0, 0, ww, barH + pad)
|
||||
col(PAL.stroke, 0.35)
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.line(0, barH + pad, ww, barH + pad)
|
||||
|
||||
love.graphics.setFont(Editor.fonts.title)
|
||||
col(PAL.white)
|
||||
love.graphics.print("Touch Controls", pad, pad + 4 * s)
|
||||
|
||||
-- Done / Reset
|
||||
local done = { x = ww - pad - btnW, y = pad + (barH - btnH) / 2,
|
||||
w = btnW, h = btnH }
|
||||
local reset = { x = done.x - 10 * s - btnW, y = done.y, w = btnW, h = btnH }
|
||||
Editor.rects.done, Editor.rects.reset = done, reset
|
||||
|
||||
local function chromeBtn(r, label, fill)
|
||||
col(fill, 0.9)
|
||||
roundRect("fill", r.x, r.y, r.w, r.h, 8 * s)
|
||||
col(PAL.white, 0.2)
|
||||
roundRect("line", r.x, r.y, r.w, r.h, 8 * s)
|
||||
love.graphics.setFont(Editor.fonts.btn)
|
||||
col(PAL.white)
|
||||
local tw = Editor.fonts.btn:getWidth(label)
|
||||
love.graphics.print(label, r.x + (r.w - tw) / 2,
|
||||
r.y + (r.h - Editor.fonts.btn:getHeight()) / 2)
|
||||
end
|
||||
chromeBtn(reset, "Reset", { 60, 70, 110 })
|
||||
chromeBtn(done, "Done", PAL.green)
|
||||
|
||||
-- enable toggle card
|
||||
local cardY = barH + pad + 14 * s
|
||||
local cardH = 64 * s
|
||||
local cardX, cardW = pad, ww - 2 * pad
|
||||
col(PAL.card, 0.88)
|
||||
roundRect("fill", cardX, cardY, cardW, cardH, 12 * s)
|
||||
col(PAL.stroke, 0.4)
|
||||
roundRect("line", cardX, cardY, cardW, cardH, 12 * s)
|
||||
|
||||
love.graphics.setFont(Editor.fonts.body)
|
||||
col(PAL.label)
|
||||
love.graphics.print("On-screen controls", cardX + 16 * s,
|
||||
cardY + 12 * s)
|
||||
love.graphics.setFont(Editor.fonts.btn)
|
||||
local on = Editor.enabled
|
||||
col(on and PAL.green or PAL.red)
|
||||
love.graphics.print(on and "ON" or "OFF", cardX + 16 * s,
|
||||
cardY + 34 * s)
|
||||
|
||||
local toggleW = 110 * s
|
||||
local toggle = {
|
||||
x = cardX + cardW - 16 * s - toggleW,
|
||||
y = cardY + (cardH - btnH) / 2,
|
||||
w = toggleW, h = btnH,
|
||||
}
|
||||
Editor.rects.toggle = toggle
|
||||
chromeBtn(toggle, on and "Disable" or "Enable",
|
||||
on and PAL.red or PAL.green)
|
||||
|
||||
-- hint
|
||||
love.graphics.setFont(Editor.fonts.body)
|
||||
col(PAL.label, 0.9)
|
||||
local hint = on
|
||||
and "Drag each button to reposition. Layout is saved when you tap Done."
|
||||
or "Controls are hidden in-game. Enable them to show and edit the layout."
|
||||
love.graphics.printf(hint, pad, cardY + cardH + 12 * s, ww - 2 * pad, "left")
|
||||
|
||||
-- the overlay itself (preview mode; dimmed when disabled)
|
||||
TouchControls:draw()
|
||||
|
||||
-- highlight the control under drag
|
||||
if Editor.drag then
|
||||
local L = TouchControls:layout()
|
||||
local zone = L[Editor.drag.name]
|
||||
if zone then
|
||||
love.graphics.setLineWidth(3 * s)
|
||||
col(PAL.green, 0.85)
|
||||
love.graphics.circle("line", zone.cx, zone.cy, zone.w * 0.62)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function beginDrag(id, x, y)
|
||||
-- chrome takes priority over controls
|
||||
if inside(Editor.rects.done, x, y) then close(); return end
|
||||
if inside(Editor.rects.reset, x, y) then resetLayout(); return end
|
||||
if inside(Editor.rects.toggle, x, y) then toggleEnabled(); return end
|
||||
|
||||
local name = TouchControls:hitTest(x, y)
|
||||
if not name then return end
|
||||
local zone = TouchControls:layout()[name]
|
||||
Editor.drag = {
|
||||
name = name,
|
||||
touchId = id,
|
||||
offX = x - zone.cx,
|
||||
offY = y - zone.cy,
|
||||
}
|
||||
end
|
||||
|
||||
local function moveDrag(id, x, y)
|
||||
local d = Editor.drag
|
||||
if not d then return end
|
||||
if d.touchId ~= nil and id ~= nil and d.touchId ~= id then return end
|
||||
TouchControls:setControlCenter(d.name, x - d.offX, y - d.offY)
|
||||
end
|
||||
|
||||
local function endDrag(id)
|
||||
local d = Editor.drag
|
||||
if not d then return end
|
||||
if d.touchId ~= nil and id ~= nil and d.touchId ~= id then return end
|
||||
Editor.drag = nil
|
||||
end
|
||||
|
||||
function Editor.mousepressed(x, y, button)
|
||||
if button ~= 1 then return end
|
||||
beginDrag("mouse", x, y)
|
||||
end
|
||||
|
||||
function Editor.mousemoved(x, y)
|
||||
if love.mouse.isDown(1) then moveDrag("mouse", x, y) end
|
||||
end
|
||||
|
||||
function Editor.mousereleased(x, y, button)
|
||||
if button ~= 1 then return end
|
||||
endDrag("mouse")
|
||||
end
|
||||
|
||||
function Editor.touchpressed(id, x, y)
|
||||
beginDrag(id, x, y)
|
||||
end
|
||||
|
||||
function Editor.touchmoved(id, x, y)
|
||||
moveDrag(id, x, y)
|
||||
end
|
||||
|
||||
function Editor.touchreleased(id, x, y)
|
||||
endDrag(id)
|
||||
end
|
||||
|
||||
function Editor.keypressed(key)
|
||||
if key == "escape" or key == "return" or key == "space" then
|
||||
close()
|
||||
elseif key == "r" then
|
||||
resetLayout()
|
||||
end
|
||||
end
|
||||
|
||||
return Editor
|
||||
@@ -1674,6 +1674,16 @@ do
|
||||
local optBack = SD.loadOptions()
|
||||
eq(optBack.zoom, -2, "options.lua round-trips zoom")
|
||||
eq(optBack.voidFill, "water", "options.lua round-trips voidFill")
|
||||
-- touchControls (#327): enabled flag + normalized positions
|
||||
rep.options.touchControls = {
|
||||
enabled = false,
|
||||
positions = { dpad = { x = 0.2, y = 0.8 }, a = { x = 0.9, y = 0.7 } },
|
||||
}
|
||||
SD.saveOptions(rep.options)
|
||||
optBack = SD.loadOptions()
|
||||
eq(optBack.touchControls.enabled, false, "options.lua round-trips touchControls.enabled")
|
||||
eq(optBack.touchControls.positions.dpad.x, 0.2,
|
||||
"options.lua round-trips touchControls.positions")
|
||||
local origOpts, loadedOpts = rep.options, back.options
|
||||
rep.options, back.options = nil, nil
|
||||
local same, where = deepEq(rep, back, "save")
|
||||
@@ -1683,6 +1693,68 @@ do
|
||||
SD.saveOptions(SD.defaultOptions())
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- touch controls layout (#327)
|
||||
do
|
||||
local TC = require("src.core.TouchControls")
|
||||
local cfg = TC.normalizeConfig(nil)
|
||||
eq(cfg.enabled, true, "touchControls default enabled")
|
||||
check(cfg.positions == nil, "touchControls default positions nil")
|
||||
|
||||
cfg = TC.normalizeConfig({
|
||||
enabled = false,
|
||||
positions = {
|
||||
dpad = { x = 1.5, y = -0.2 }, -- clamped
|
||||
a = { x = 0.5, y = 0.5 },
|
||||
junk = { x = 0, y = 0 },
|
||||
b = { x = "nope", y = 0.1 },
|
||||
},
|
||||
})
|
||||
eq(cfg.enabled, false, "normalizeConfig keeps enabled=false")
|
||||
eq(cfg.positions.dpad.x, 1, "normalizeConfig clamps x high")
|
||||
eq(cfg.positions.dpad.y, 0, "normalizeConfig clamps y low")
|
||||
eq(cfg.positions.a.x, 0.5, "normalizeConfig keeps a")
|
||||
check(cfg.positions.junk == nil, "normalizeConfig drops unknown controls")
|
||||
check(cfg.positions.b == nil, "normalizeConfig drops non-numeric")
|
||||
|
||||
local L = TC.defaultLayout(400, 800)
|
||||
check(L.dpad.cx < 200, "default d-pad on left half")
|
||||
check(L.a.cx > 200, "default A on right half")
|
||||
check(L.dpad.cy > 400, "default d-pad in bottom half")
|
||||
|
||||
-- applyOptions + visible gate (no real images needed for the gate)
|
||||
TC.enabled = true
|
||||
TC.active = true
|
||||
TC.img = {} -- pretend art loaded
|
||||
TC.controllerHidden = false
|
||||
TC.preview = false
|
||||
TC:applyOptions({ touchControls = { enabled = false } })
|
||||
eq(TC.enabled, false, "applyOptions disables overlay")
|
||||
check(not TC:visible(), "disabled overlay is not visible")
|
||||
TC:applyOptions({
|
||||
touchControls = {
|
||||
enabled = true,
|
||||
positions = { dpad = { x = 0.25, y = 0.75 } },
|
||||
},
|
||||
})
|
||||
eq(TC.enabled, true, "applyOptions re-enables overlay")
|
||||
eq(TC.positions.dpad.x, 0.25, "applyOptions stores positions")
|
||||
check(TC:visible(), "enabled overlay is visible when active+imaged")
|
||||
|
||||
-- custom position applied through layout()
|
||||
local g = love.graphics
|
||||
local oldDim, oldFont = g.getDimensions, g.newFont
|
||||
g.getDimensions = function() return 400, 800 end
|
||||
g.newFont = function() return { getWidth = function() return 10 end,
|
||||
getHeight = function() return 10 end } end
|
||||
TC.layoutW, TC.layoutH, TC.L = nil, nil, nil
|
||||
local lay = TC:layout()
|
||||
eq(lay.dpad.cx, 100, "custom dpad cx = nx * ww")
|
||||
eq(lay.dpad.cy, 600, "custom dpad cy = ny * wh")
|
||||
TC:clearPositions()
|
||||
check(TC.positions == nil, "clearPositions wipes overrides")
|
||||
g.getDimensions, g.newFont = oldDim, oldFont
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- crit thresholds (CriticalHitTest)
|
||||
-- The threshold byte b from engine/battle/core.asm's shift chain:
|
||||
-- srl (speed/2), then sla (cap 255) without Focus Energy or srl with the
|
||||
|
||||
Reference in New Issue
Block a user