Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -962,18 +962,33 @@ function love.mousereleased(x, y, button, istouch)
if editorMode and EditorApp.mousereleased then
return EditorApp.mousereleased(x, y, button)
end
if Game and (mouseTouch or Game:hasPointerScreen()) and button == 1 and not istouch then
Game:touchreleased("mouse", x, y)
if mouseTouch then
if Game and button == 1 then pcall(function() Game:touchreleased("mouse", x, y) end) end
return
end
if Game and Game.mousereleased and not istouch then
Game:mousereleased(x, y, button, istouch)
end
if Game and Game.recenterLookPointer and not istouch then
Game:recenterLookPointer()
end
end

function love.mousemoved(x, y, dx, dy, istouch)
-- FREE-CAMERA LOOK FIRST, and only when one is up: `Game:cameraLook`
-- answers false on every other map and camera, so the touch path below
-- keeps every case it had.
-- keeps every case it had. Warp back to centre after a real look so the
-- pointer never sits on the window edge (where dx/dy become zero).
if Game and Game.lookWarping then
Game.lookWarping = false
return
end
if Game and not istouch and not (editorMode or Importer or TouchEditor)
and not Game:hasPointerScreen()
and Game.cameraLook and Game:cameraLook(dx, dy) then
if ((dx or 0) ~= 0 or (dy or 0) ~= 0) and Game.recenterLookPointer then
Game:recenterLookPointer()
end
return
end
if TouchEditor then
Expand Down
159 changes: 82 additions & 77 deletions src/core/Game.lua
Original file line number Diff line number Diff line change
Expand Up @@ -416,72 +416,82 @@ Game.LOOK_PER_PIXEL = 0.20
Game.LOOK_RISE_PER_PIXEL = 0.12
Game.LOOK_PER_SECOND = 150
Game.LOOK_RISE_PER_SECOND = 80
Game.TOUCH_LOOK_MULTIPLIER = 3.0
Game.TOUCH_RISE_MULTIPLIER = 3.0
-- Below this the stick is treated as centred, and past it the response starts
-- from zero rather than jumping -- otherwise a worn stick drifts the camera
-- round on its own and a small push snaps.
Game.STICK_DEADZONE = 0.22

-- Mouse look. Returns true when it consumed the movement.
-- THE LARGEST TURN ONE MOUSE EVENT MAY ASK FOR, in degrees.
--
-- A hand cannot move the mouse this far between two frames. At 60 Hz a fast
-- flick of a thousand pixels a second is about sixteen pixels an event, which
-- at `LOOK_PER_PIXEL` is 3.3 degrees; twelve is nearly four times that, so no
-- real movement is ever refused. What DOES produce a delta this size is the
-- pointer being warped, and SDL emits exactly one enormous `mousemoved` when
-- relative mode is turned on or off.
Game.LOOK_MAX_DEGREES = 12

-- How many events to ignore after relative mode is toggled.
Game.LOOK_SETTLE_EVENTS = 2

function Game:cameraLook(dx, dy)
-- Accumulated mouse deltas for Gen4 camera
Game.cameraLookDX = 0
Game.cameraLookDY = 0

local function lookPointerWanted(self)
if not self:freeView() then return false end
if (self.stack and self.stack:top()) ~= self.overworld then return false end
if love.window and love.window.hasFocus then
local ok, focused = pcall(love.window.hasFocus)
return ok and focused or false
end
return true
end

-- Confine the look pointer: relative deltas, and grab
-- plus a warp to the window centre so look never dies on the frame edge.
local function syncLookPointer(self, want)
if not (love and love.mouse) then
self.lookRelative = false
return
end
local mouse = love.mouse
local changed = (self.lookRelative == true) ~= (want == true)
if mouse.setRelativeMode then
pcall(mouse.setRelativeMode, want)
end
if mouse.setGrabbed then
pcall(mouse.setGrabbed, want)
end
if mouse.setVisible then
pcall(mouse.setVisible, not want)
end
self.lookRelative = want and true or false
if changed then
self.lookSettle = Game.LOOK_SETTLE_EVENTS
end
end

function Game:recenterLookPointer()
if not lookPointerWanted(self) then return end
if not (love.mouse and love.mouse.setPosition) then return end
local w, h = 0, 0
if love.graphics and love.graphics.getDimensions then
w, h = love.graphics.getDimensions()
elseif love.window and love.window.getMode then
w, h = love.window.getMode()
end
if not (w and h) or w <= 0 or h <= 0 then return end

self.lookWarping = true
pcall(love.mouse.setPosition, w * 0.5, h * 0.5)
end

function Game:cameraLook(dx, dy, isTouch)
local view = self:freeView()
if not view then return false end
if (dx == 0 or dx == nil) and (dy == 0 or dy == nil) then return true end

-- A WARP IS NOT A LOOK.
--
-- HONESTY FIRST: this guard was added as a proposed cause of the reported
-- camera flip, and that proposal was WRONG. The flip was two maps each
-- owning a `Gen4View` and the frame being drawn through whichever one was
-- current -- see the note in `Gen4View:follow`, which measures the two
-- disagreeing by 84 degrees. The guard was instrumented to say so, and the
-- play test that found the real cause never printed its line.
--
-- It is kept, and only for what it is: relative mouse mode is toggled every
-- time the overworld stops or starts being the top screen, each toggle warps
-- the pointer, and SDL reports a warp as one `mousemoved` carrying a delta
-- the width of the window. That is a real event whether or not it was the
-- flip. A hand cannot move twelve degrees between two frames -- a fast
-- flick of a thousand pixels a second is 3.3 degrees an event -- so this
-- cannot refuse a real movement. If it ever does, delete it rather than
-- raise the cap: it is a net, not a feature.
--
-- DROPPED rather than clamped: a clamped warp still turns the camera twelve
-- degrees for no reason, and the event is not a small movement that got too
-- big, it is not a movement at all.
if (self.lookSettle or 0) > 0 then
self.lookSettle = self.lookSettle - 1
return true
end
local turn = math.abs((dx or 0) * Game.LOOK_PER_PIXEL)
local lift = math.abs((dy or 0) * Game.LOOK_RISE_PER_PIXEL)
if turn > Game.LOOK_MAX_DEGREES or lift > Game.LOOK_MAX_DEGREES then
-- SAID ONCE, because this is a hypothesis about a report and the next play
-- test is what confirms it. If the flip stops and this line never
-- appears, the cause was something else and this guard is dead weight.
if not Game.saidLookSpike then
Game.saidLookSpike = true
Logger.info("gen4 camera: dropped a mouse look of %.1f/%.1f degrees "
.. "(cap %.1f) -- a pointer warp, not a hand",
turn, lift, Game.LOOK_MAX_DEGREES)
end
return true
end
if (dx == 0 or dx == nil) and (dy == 0 or dy == nil) then return true end

local multX = isTouch and Game.TOUCH_LOOK_MULTIPLIER or 1
local multY = isTouch and Game.TOUCH_RISE_MULTIPLIER or 1

Game.cameraLookDX = (Game.cameraLookDX or 0) + ((dx or 0) * multX)
Game.cameraLookDY = (Game.cameraLookDY or 0) + ((dy or 0) * multY)

view:orbit((dx or 0) * Game.LOOK_PER_PIXEL,
(dy or 0) * Game.LOOK_RISE_PER_PIXEL)
return true
end

Expand All @@ -501,33 +511,25 @@ end
function Game:updateCameraStick(dt)
local view = self:freeView()
if not view then
if self.lookRelative and love.mouse and love.mouse.setRelativeMode then
pcall(love.mouse.setRelativeMode, false)
self.lookRelative = false
self.lookSettle = Game.LOOK_SETTLE_EVENTS
end
if self.lookRelative then syncLookPointer(self, false) end
Game.cameraLookDX = 0
Game.cameraLookDY = 0
return
end
-- RELATIVE MODE, or the pointer hits the edge of the window and the look
-- stops dead. Only while the overworld itself is on top: a menu over a free
-- camera still wants a real cursor.
local wantRelative = (self.stack and self.stack:top()) == self.overworld
if wantRelative and love.window and love.window.hasFocus then
local ok, focused = pcall(love.window.hasFocus)
wantRelative = ok and focused or false
-- Relative mode, grab, and a warp to centre: without those the pointer
-- hits the window edge and look stops. A menu over a free camera still
-- wants a real cursor, so capture only while the overworld is on top.
local wantRelative = lookPointerWanted(self)
if (self.lookRelative == true) ~= (wantRelative == true) then
syncLookPointer(self, wantRelative)
if wantRelative then self:recenterLookPointer() end
end
local actualRelative = self.lookRelative
if love.mouse and love.mouse.getRelativeMode then
local ok, relative = pcall(love.mouse.getRelativeMode)
if ok then actualRelative = relative end
end
if love.mouse and love.mouse.setRelativeMode and actualRelative ~= wantRelative then
local ok = pcall(love.mouse.setRelativeMode, wantRelative)
self.lookRelative = ok and wantRelative or false
-- ...and ignore what the warp this causes is about to report.
self.lookSettle = Game.LOOK_SETTLE_EVENTS
else
self.lookRelative = actualRelative

local dx, dy = Game.cameraLookDX or 0, Game.cameraLookDY or 0
Game.cameraLookDX = 0
Game.cameraLookDY = 0
if dx ~= 0 or dy ~= 0 then
view:orbit(dx * Game.LOOK_PER_PIXEL, dy * Game.LOOK_RISE_PER_PIXEL)
end
if not (love.joystick and love.joystick.getJoysticks) then return end
local ok, pads = pcall(love.joystick.getJoysticks)
Expand Down Expand Up @@ -1648,6 +1650,9 @@ function Game:touchpressed(id, x, y)
end

function Game:hasPointerScreen()
-- Allow camera look in Gen4 free camera mode even if a pointer screen exists
-- (e.g., Poketch visible but not actively interacting with UI)
if self:freeView() then return false end
if self.pointerOwners and self.pointerOwners.mouse then return true end
local top=self.stack and self.stack.top and self.stack:top()
return top and type(top.touchpressed)=='function' or false
Expand Down
50 changes: 45 additions & 5 deletions src/core/TouchControls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ function TouchControls:init()
-- and lifting one of them must not release the other's hold
self.held = {}
self.dpadTouch = nil
self.cameraLookTouch = nil
self.cameraLookStart = nil
self.leftStickTouch = nil
self.rightStickTouch = nil
self.layoutW, self.layoutH = nil, nil
self.img = nil
-- Images load whenever the platform wants the overlay OR the launcher
Expand Down Expand Up @@ -482,23 +486,55 @@ function TouchControls:touchpressed(id, x, y)
self.touches[id] = touch
setDpad(self, touch, dpadDir(dz, x, y))
end

-- Gen4 free camera: touch drag on open screen controls camera
local Game = require("src.core.Game")
if Game and Game.freeView and Game:freeView() then
self.cameraLookTouch = id
self.cameraLookStart = { x = x, y = y }
self.touches[id] = { control = "cameralook" }
return true
end
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
if not touch or touch.control ~= "dpad" then return end
setDpad(self, touch, dpadDir(self:layout().dpad, x, y))
if not touch then return end

-- Gen4 free camera look
if touch.control == "cameralook" and self.cameraLookStart then
local dx = x - self.cameraLookStart.x
local dy = y - self.cameraLookStart.y
self.cameraLookStart = { x = x, y = y }
local Game = require("src.core.Game")
if Game and Game.cameraLook then
Game:cameraLook(dx, dy, true)
end
return
end

-- the d-pad and left stick both track movement (slide between
-- directions without lifting); buttons hold until release wherever the finger wanders
if touch.control == "dpad" then
setDpad(self, touch, dpadDir(self:layout()[touch.control], x, y))
elseif touch.control == "leftstick" then
setLeftStick(self, touch, dpadDir(self:layout()[touch.control], x, y))
elseif touch.control == "rightstick" then
setRightStickDir(self, touch, dpadDir(self:layout().rightstick, x, y))
end
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
if touch.control == "dpad" then

if touch.control == "cameralook" then
self.cameraLookTouch = nil
self.cameraLookStart = nil
elseif touch.control == "dpad" then
setDpad(self, touch, nil)
self.dpadTouch = nil
else
Expand All @@ -517,6 +553,10 @@ function TouchControls:reset()
self.held = {}
self.touches = {}
self.dpadTouch = nil
self.leftStickTouch = nil
self.rightStickTouch = nil
self.cameraLookTouch = nil
self.cameraLookStart = nil
end

-- a gamepad is being used: hide the overlay (dropping anything it held)
Expand Down
Loading