mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
skins and skin studio
This commit is contained in:
+219
-7
@@ -39,6 +39,7 @@
|
||||
|
||||
local Input = require("src.core.Input")
|
||||
local SafeArea = require("src.core.SafeArea")
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
|
||||
local TouchControls = {}
|
||||
|
||||
@@ -189,6 +190,7 @@ function TouchControls.normalizeConfig(tc)
|
||||
-- no caller ever has to nil-check a bucket's scale
|
||||
if type(tc) ~= "table" then tc = {} end
|
||||
if tc.enabled == false then out.enabled = false end
|
||||
if type(tc.skin) == "string" and tc.skin ~= "" then out.skin = tc.skin end
|
||||
local saved = type(tc.layouts) == "table" and tc.layouts or nil
|
||||
for _, o in ipairs(ORIENTATIONS) do
|
||||
local b = saved and saved[o]
|
||||
@@ -241,6 +243,7 @@ end
|
||||
|
||||
function TouchControls:init()
|
||||
self.active = wantsOverlay()
|
||||
TouchSkin.setOverlayLive(self.active)
|
||||
self.enabled = true
|
||||
-- vibration level for presses (#806); applyOptions overwrites it from
|
||||
-- options.haptics, this is the value a harness that never applies options
|
||||
@@ -261,6 +264,10 @@ function TouchControls:init()
|
||||
self.held = {}
|
||||
self.dpadTouch = nil
|
||||
self.layoutW, self.layoutH = nil, nil
|
||||
self.skinId = nil
|
||||
self.skinError = nil
|
||||
self.hotkeysHeld = {}
|
||||
TouchSkin.setActive(nil)
|
||||
self.img = nil
|
||||
-- Images load whenever the platform wants the overlay OR the launcher
|
||||
-- editor forces a preview (desktop testing of the editor).
|
||||
@@ -285,6 +292,8 @@ function TouchControls:applyOptions(opts)
|
||||
-- haptics is a plain top-level option, not part of the layout config the
|
||||
-- launcher editor round-trips through config() (#806)
|
||||
self.haptics = TouchControls.normalizeHaptics(opts and opts.haptics)
|
||||
TouchSkin.setOverlayLive(self.active)
|
||||
self:selectSkin(cfg.skin)
|
||||
self.layouts = cfg.layouts
|
||||
self.layoutW, self.layoutH = nil, nil
|
||||
self.layoutOx, self.layoutOy = nil, nil
|
||||
@@ -300,7 +309,7 @@ end
|
||||
-- Snapshot for the editor's save path: enabled plus both orientation
|
||||
-- buckets, matching what options.lua stores (#633).
|
||||
function TouchControls:config()
|
||||
local out = { enabled = self.enabled ~= false, layouts = {} }
|
||||
local out = { enabled = self.enabled ~= false, skin = self.skinId, layouts = {} }
|
||||
for _, o in ipairs(ORIENTATIONS) do
|
||||
local b = self.layouts and self.layouts[o] or nil
|
||||
out.layouts[o] = {
|
||||
@@ -323,11 +332,56 @@ function TouchControls:setPreview(on)
|
||||
end
|
||||
|
||||
function TouchControls:visible()
|
||||
if self.preview then return self.img ~= nil end
|
||||
return self.active and self.enabled ~= false and self.img ~= nil
|
||||
and not self.controllerHidden
|
||||
local art = TouchSkin.active ~= nil or self.img ~= nil
|
||||
if self.preview then return art end
|
||||
if self.enabled == false or not art then return false end
|
||||
if TouchSkin.active and TouchSkin.decorativeOnly() then return true end
|
||||
return self.active and not self.controllerHidden
|
||||
end
|
||||
|
||||
local function surfaceRect()
|
||||
local r = TouchSkin.surfaceRect
|
||||
if r then return r.w, r.h, r.x, r.y end
|
||||
if love and love.graphics and love.graphics.getDimensions then
|
||||
local w, h = love.graphics.getDimensions()
|
||||
return w, h, 0, 0
|
||||
end
|
||||
return 0, 0, 0, 0
|
||||
end
|
||||
|
||||
TouchControls.surfaceRect = surfaceRect
|
||||
|
||||
function TouchControls:selectSkin(id)
|
||||
if id == self.skinId and TouchSkin.active then return TouchSkin.active end
|
||||
self:reset()
|
||||
self.skinError = nil
|
||||
if not id or id == "" then
|
||||
self.skinId = nil
|
||||
TouchSkin.setActive(nil)
|
||||
return nil
|
||||
end
|
||||
local skin, err = TouchSkin.select(id)
|
||||
if not skin then
|
||||
self.skinId = nil
|
||||
self.skinError = err
|
||||
TouchSkin.setActive(nil)
|
||||
return nil, err
|
||||
end
|
||||
self.skinId = id
|
||||
return skin
|
||||
end
|
||||
|
||||
function TouchControls:skin()
|
||||
if not self:visible() then return nil end
|
||||
return TouchSkin.active
|
||||
end
|
||||
|
||||
function TouchControls:setHotkeyHandler(fn)
|
||||
self.hotkeyHandler = type(fn) == "function" and fn or nil
|
||||
end
|
||||
|
||||
local skinHitSet, applySkinSet
|
||||
|
||||
-- Keep a control fully inside the usable rect [x0,y0]..[x1,y1].
|
||||
local function clampZone(zone, x0, y0, x1, y1)
|
||||
local half = zone.w * 0.5
|
||||
@@ -451,6 +505,15 @@ 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 page = TouchSkin.page()
|
||||
if page then
|
||||
local ww, wh, ox, oy = surfaceRect()
|
||||
for i = #page.controls, 1, -1 do
|
||||
local ctl = page.controls[i]
|
||||
if TouchSkin.hits(page, ctl, ww, wh, x, y, ox, oy) then return ctl.spec, ctl end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local L = self:layout()
|
||||
for _, btn in ipairs(BUTTONS) do
|
||||
if inCircle(L[btn], x, y, SLOP[btn]) then return btn end
|
||||
@@ -505,6 +568,86 @@ local function setDpad(self, touch, dir)
|
||||
if dir then pressBtn(self, dir) end
|
||||
end
|
||||
|
||||
local function pressKey(key, down)
|
||||
local fn = down and love.keypressed or love.keyreleased
|
||||
if type(fn) == "function" then pcall(fn, key, key, false) end
|
||||
end
|
||||
|
||||
local function fireHotkey(self, action, pressed, ctl)
|
||||
if action == "overlay_next" then
|
||||
if pressed then TouchSkin.nextPage(ctl and ctl.nextTarget) end
|
||||
return pressed
|
||||
end
|
||||
if action == "overlay_prev" then
|
||||
if pressed then TouchSkin.setPage(TouchSkin.pageIndex - 1) end
|
||||
return pressed
|
||||
end
|
||||
self.hotkeysHeld = self.hotkeysHeld or {}
|
||||
local n = (self.hotkeysHeld[action] or 0) + (pressed and 1 or -1)
|
||||
if n < 0 then n = 0 end
|
||||
self.hotkeysHeld[action] = n > 0 and n or nil
|
||||
local edge = (pressed and n == 1) or (not pressed and n == 0)
|
||||
if edge and self.hotkeyHandler then
|
||||
pcall(self.hotkeyHandler, action, pressed)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function enterControl(self, ctl)
|
||||
local buzzed = false
|
||||
for _, btn in ipairs(ctl.buttons) do
|
||||
if not self.held[btn] then buzzed = true end
|
||||
pressBtn(self, btn)
|
||||
end
|
||||
for _, key in ipairs(ctl.keys) do pressKey(key, true) end
|
||||
local switched = false
|
||||
for _, action in ipairs(ctl.hotkeys) do
|
||||
if fireHotkey(self, action, true, ctl) then switched = true end
|
||||
end
|
||||
if not buzzed and (ctl.keys[1] or ctl.hotkeys[1]) then
|
||||
TouchControls.buzz(self.haptics)
|
||||
end
|
||||
return switched
|
||||
end
|
||||
|
||||
local function exitControl(self, ctl)
|
||||
for _, btn in ipairs(ctl.buttons) do releaseBtn(self, btn) end
|
||||
for _, key in ipairs(ctl.keys) do pressKey(key, false) end
|
||||
for _, action in ipairs(ctl.hotkeys) do fireHotkey(self, action, false, ctl) end
|
||||
end
|
||||
|
||||
function skinHitSet(self, x, y)
|
||||
local page = TouchSkin.page()
|
||||
if not page then return nil end
|
||||
local ww, wh, ox, oy = surfaceRect()
|
||||
local set = nil
|
||||
for _, ctl in ipairs(page.controls) do
|
||||
if not ctl.decorative and TouchSkin.hits(page, ctl, ww, wh, x, y, ox, oy) then
|
||||
set = set or {}
|
||||
set[ctl] = true
|
||||
end
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
function applySkinSet(self, touch, set)
|
||||
local prev = touch.set or {}
|
||||
local switched = false
|
||||
for ctl in pairs(prev) do
|
||||
if not (set and set[ctl]) then exitControl(self, ctl) end
|
||||
end
|
||||
for ctl in pairs(set or {}) do
|
||||
if not prev[ctl] then
|
||||
if enterControl(self, ctl) then switched = true end
|
||||
end
|
||||
end
|
||||
touch.set = set
|
||||
if switched then
|
||||
for ctl in pairs(touch.set or {}) do exitControl(self, ctl) end
|
||||
touch.set = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true when this touch was captured by a virtual control -- the
|
||||
-- pad's first refusal on the gameplay pointer seam (#807). Capture is
|
||||
-- decided here, at press, and rides self.touches[id] for the touch's
|
||||
@@ -513,13 +656,22 @@ end
|
||||
function TouchControls:touchpressed(id, x, y)
|
||||
-- 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
|
||||
if not (self.active and self.enabled ~= false
|
||||
and (TouchSkin.active or self.img)) then return end
|
||||
-- a controller hid the overlay; the first touch only brings it back
|
||||
-- (uncaptured: it began on no control, so mods may still see it)
|
||||
if self.controllerHidden then
|
||||
self.controllerHidden = false
|
||||
return
|
||||
end
|
||||
if TouchSkin.active then
|
||||
local set = skinHitSet(self, x, y)
|
||||
if not set then return end
|
||||
local touch = { control = "skin" }
|
||||
self.touches[id] = touch
|
||||
applySkinSet(self, touch, set)
|
||||
return true
|
||||
end
|
||||
local L = self:layout()
|
||||
for _, btn in ipairs(BUTTONS) do
|
||||
if inCircle(L[btn], x, y, SLOP[btn]) then
|
||||
@@ -544,9 +696,14 @@ end
|
||||
function TouchControls:touchmoved(id, x, y)
|
||||
if self.preview then return end
|
||||
local touch = self.touches[id]
|
||||
if not touch then return end
|
||||
if touch.control == "skin" then
|
||||
applySkinSet(self, touch, skinHitSet(self, x, y))
|
||||
return
|
||||
end
|
||||
-- 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
|
||||
if touch.control ~= "dpad" then return end
|
||||
setDpad(self, touch, dpadDir(self:layout().dpad, x, y))
|
||||
end
|
||||
|
||||
@@ -555,7 +712,9 @@ function TouchControls:touchreleased(id, x, y)
|
||||
local touch = self.touches[id]
|
||||
if not touch then return end
|
||||
self.touches[id] = nil
|
||||
if touch.control == "dpad" then
|
||||
if touch.control == "skin" then
|
||||
applySkinSet(self, touch, nil)
|
||||
elseif touch.control == "dpad" then
|
||||
setDpad(self, touch, nil)
|
||||
self.dpadTouch = nil
|
||||
else
|
||||
@@ -568,6 +727,15 @@ end
|
||||
-- touchreleased and would strand its button held forever. Called from
|
||||
-- Game alongside Input:reset() on focus/visibility loss.
|
||||
function TouchControls:reset()
|
||||
for _, touch in pairs(self.touches or {}) do
|
||||
if touch.control == "skin" and touch.set then
|
||||
for ctl in pairs(touch.set) do exitControl(self, ctl) end
|
||||
end
|
||||
end
|
||||
for action in pairs(self.hotkeysHeld or {}) do
|
||||
if self.hotkeyHandler then pcall(self.hotkeyHandler, action, false) end
|
||||
end
|
||||
self.hotkeysHeld = {}
|
||||
for btn in pairs(self.held or {}) do
|
||||
Input:overlayReleased(btn)
|
||||
end
|
||||
@@ -608,12 +776,56 @@ local function drawIcon(img, zone, pressed, alphaMul)
|
||||
zone.cy - img:getHeight() * scale / 2, 0, scale, scale)
|
||||
end
|
||||
|
||||
local function drawStretched(img, x, y, w, h, alpha)
|
||||
if not img or alpha <= 0 then return end
|
||||
local iw, ih = img:getWidth(), img:getHeight()
|
||||
if iw <= 0 or ih <= 0 then return end
|
||||
love.graphics.setColor(1, 1, 1, math.min(1, alpha))
|
||||
love.graphics.draw(img, x, y, 0, w / iw, h / ih)
|
||||
end
|
||||
|
||||
function TouchControls:drawSkin(alphaMul)
|
||||
local page = TouchSkin.page()
|
||||
if not page then return false end
|
||||
local ww, wh, sox, soy = surfaceRect()
|
||||
local bx, by, bw, bh = TouchSkin.pageBox(page, ww, wh, sox, soy)
|
||||
local opacity = (self.skinOpacity or 1) * alphaMul
|
||||
|
||||
love.graphics.push("all")
|
||||
love.graphics.origin()
|
||||
drawStretched(page.image, bx, by, bw, bh, opacity)
|
||||
|
||||
local pressed = {}
|
||||
for _, touch in pairs(self.touches or {}) do
|
||||
for ctl in pairs(touch.set or {}) do pressed[ctl] = true end
|
||||
end
|
||||
|
||||
for _, ctl in ipairs(page.controls) do
|
||||
local down = pressed[ctl] == true
|
||||
local img = (down and ctl.pressedImage) or ctl.image
|
||||
if img then
|
||||
local cx, cy, halfW, halfH =
|
||||
TouchSkin.controlGeometry(page, ctl, ww, wh, sox, soy)
|
||||
local alpha = opacity
|
||||
if down and not ctl.pressedImage then alpha = opacity * ctl.alphaMod end
|
||||
drawStretched(img, cx - halfW, cy - halfH, halfW * 2, halfH * 2, alpha)
|
||||
end
|
||||
end
|
||||
|
||||
love.graphics.pop()
|
||||
return true
|
||||
end
|
||||
|
||||
-- OS-window space, called after GameViewport.finish so the overlay rides on
|
||||
-- top of the game, companion composition and post-processing without being
|
||||
-- captured or scaled with any game viewport. Also used by the launcher layout
|
||||
-- editor under preview mode.
|
||||
function TouchControls:draw()
|
||||
if not self:visible() then return end
|
||||
if TouchSkin.active then
|
||||
local mul = (self.preview and self.enabled == false) and 0.45 or 1
|
||||
if self:drawSkin(mul) then return end
|
||||
end
|
||||
local L = self:layout()
|
||||
-- when the player disabled the overlay but the editor is previewing,
|
||||
-- draw dimmed so the layout is still editable
|
||||
|
||||
Reference in New Issue
Block a user