mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 13:36:14 +02:00
skins
This commit is contained in:
+13
-6
@@ -294,7 +294,6 @@ function TouchSkin.parse(text)
|
||||
page.viewport = { x = num(vp[1], 0), y = num(vp[2], 0),
|
||||
w = num(vp[3], 1), h = num(vp[4], 1) }
|
||||
page.viewportFill = toBool(kv[p .. "_viewport_fill"])
|
||||
page.viewportExpand = toBool(kv[p .. "_viewport_expand"])
|
||||
end
|
||||
|
||||
page.pixelCoords = not page.normalized
|
||||
@@ -447,7 +446,6 @@ function TouchSkin.parseNative(text)
|
||||
page.viewport = { x = num(raw.viewport.x, 0), y = num(raw.viewport.y, 0),
|
||||
w = num(raw.viewport.w, 1), h = num(raw.viewport.h, 1) }
|
||||
page.viewportFill = raw.viewport.fill == true
|
||||
page.viewportExpand = raw.viewport.expand == true
|
||||
end
|
||||
for _, c in ipairs(raw.controls or {}) do
|
||||
local buttons, hotkeys, keys, decorative = parseBinds(c.bind or "nul")
|
||||
@@ -526,7 +524,6 @@ function TouchSkin.toNative(skin)
|
||||
x = page.viewport.x, y = page.viewport.y,
|
||||
w = page.viewport.w, h = page.viewport.h,
|
||||
fill = page.viewportFill or nil,
|
||||
expand = page.viewportExpand or nil,
|
||||
}
|
||||
end
|
||||
for _, ctl in ipairs(page.controls or {}) do
|
||||
@@ -922,7 +919,6 @@ function TouchSkin.toRetroArchConfig(skin)
|
||||
if page.viewport then
|
||||
out[#out + 1] = p .. "_viewport = " .. fmtRect(page.viewport)
|
||||
if page.viewportFill then out[#out + 1] = p .. "_viewport_fill = true" end
|
||||
if page.viewportExpand then out[#out + 1] = p .. "_viewport_expand = true" end
|
||||
end
|
||||
local controls = {}
|
||||
for _, ctl in ipairs(page.controls or {}) do
|
||||
@@ -1434,6 +1430,16 @@ local function remainderBox(ox, oy, w, h, bx, by, bw, bh)
|
||||
return best[1], best[2], best[3], best[4]
|
||||
end
|
||||
|
||||
-- The deck box pinned to the lower edge (see pageBox) leaves room above it
|
||||
-- that belongs to the game, so a screen rect flush with the top of the deck
|
||||
-- grows into it instead of showing a black band.
|
||||
local function deckHeadroom(y, vh, by, bh, oy, h)
|
||||
if by <= oy + 0.5 then return y, vh end
|
||||
if by + bh < oy + h - 0.5 then return y, vh end
|
||||
if y - by > math.max(2, bh * 0.01) then return y, vh end
|
||||
return oy, vh + (y - oy)
|
||||
end
|
||||
|
||||
function TouchSkin.pageViewport(page, w, h, ox, oy)
|
||||
if not page then return nil end
|
||||
ox, oy = ox or 0, oy or 0
|
||||
@@ -1443,12 +1449,13 @@ function TouchSkin.pageViewport(page, w, h, ox, oy)
|
||||
local x, y = bx + v.x * bw, by + v.y * bh
|
||||
local vw, vh = v.w * bw, v.h * bh
|
||||
if vw <= 0 or vh <= 0 then return nil end
|
||||
return x, y, vw, vh, page.viewportFill == true, page.viewportExpand == true
|
||||
y, vh = deckHeadroom(y, vh, by, bh, oy, h)
|
||||
return x, y, vw, vh, page.viewportFill == true
|
||||
end
|
||||
if page.screenFit == "remainder" then
|
||||
local x, y, vw, vh = remainderBox(ox, oy, w, h, bx, by, bw, bh)
|
||||
if not x then return nil end
|
||||
return x, y, vw, vh, false, false
|
||||
return x, y, vw, vh, false
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -1067,9 +1067,22 @@ local GAME_TABS = {
|
||||
local HEADER_TABS = {
|
||||
{ id = "mods", key = "tab-mods" },
|
||||
{ id = "find", key = "tab-find" },
|
||||
{ id = "skins", key = "tab-skins", glyph = true },
|
||||
{ id = "skins", key = "tab-skins", glyph = true, beta = true },
|
||||
{ id = "bug", key = "tab-bug" },
|
||||
}
|
||||
|
||||
local BETA_TAG_OPTS = { fill = true, bold = true, ink = PAL.inverse }
|
||||
|
||||
local function drawBetaTag(x, y, w, h)
|
||||
Kit.tag(x, y, w, h, "BETA", PAL.yellow, BETA_TAG_OPTS)
|
||||
end
|
||||
|
||||
local function overlayBeta(tx, ty, w, tabH, m)
|
||||
local bh = math.floor(11 * m.s)
|
||||
local bw = math.min(w, Kit.textWidth("micro", "BETA") + math.floor(10 * m.s))
|
||||
drawBetaTag(tx + (w - bw) / 2, ty + tabH - bh - math.floor(2 * m.s), bw, bh)
|
||||
end
|
||||
|
||||
for _, t in ipairs(HEADER_TABS) do
|
||||
t.opts = { face = "tab", font = "tab", color = t.color, letter = t.letter }
|
||||
if t.glyph then
|
||||
@@ -1273,6 +1286,7 @@ local function buildHeader(imp, m)
|
||||
o.image = t.icon
|
||||
o.action = chrome.tab[t.id]
|
||||
btn(imp, tx, ty, w, tabH, t.key, "", o)
|
||||
if t.beta then overlayBeta(tx, ty, w, tabH, m) end
|
||||
tx = tx + w + tabGap
|
||||
end
|
||||
-- The bug-report chip sits LAST, past the sync chip.
|
||||
@@ -1290,10 +1304,7 @@ local function buildHeader(imp, m)
|
||||
local o = chrome.sync
|
||||
o.active = imp._syncModal ~= nil
|
||||
btn(imp, tx, ty, w, tabH, "tab-sync", "", o)
|
||||
local bh = math.floor(11 * m.s)
|
||||
local bw = math.min(w, Kit.textWidth("micro", "BETA") + math.floor(10 * m.s))
|
||||
Kit.tag(tx + (w - bw) / 2, ty + tabH - bh - math.floor(2 * m.s), bw, bh,
|
||||
"BETA", o.active and PAL.inverse or PAL.yellow)
|
||||
overlayBeta(tx, ty, w, tabH, m)
|
||||
local eng = imp._sync
|
||||
if eng and eng.busy and eng:busy() then
|
||||
Kit.spinner(tx + w - math.floor(8 * m.s), ty + math.floor(8 * m.s),
|
||||
@@ -4490,8 +4501,8 @@ local function syncTitle(imp, m, px, py, pw, pad)
|
||||
Kit.text("button", label, px + pad, py, PAL.heading)
|
||||
local bh = math.floor(15 * m.s)
|
||||
local bw = Kit.textWidth("micro", "BETA") + math.floor(14 * m.s)
|
||||
Kit.tag(px + pad + Kit.textWidth("button", label) + math.floor(8 * m.s),
|
||||
py + (Kit.textHeight("button") - bh) / 2, bw, bh, "BETA", PAL.yellow)
|
||||
drawBetaTag(px + pad + Kit.textWidth("button", label) + math.floor(8 * m.s),
|
||||
py + (Kit.textHeight("button") - bh) / 2, bw, bh)
|
||||
return py + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
end
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ local TouchSkin = require("src.core.TouchSkin")
|
||||
|
||||
local Playfield = {}
|
||||
|
||||
Playfield.WIDTH, Playfield.HEIGHT = 160, 144
|
||||
|
||||
Playfield.entered = false
|
||||
Playfield.box = nil
|
||||
|
||||
@@ -35,14 +33,9 @@ function Playfield.cutout(sw, sh)
|
||||
end
|
||||
|
||||
function Playfield.rect(sw, sh)
|
||||
local x, y, w, h, _, expand = Playfield.cutout(sw, sh)
|
||||
local x, y, w, h = Playfield.cutout(sw, sh)
|
||||
if not x then return 0, 0, sw or 0, sh or 0, false end
|
||||
if expand then return x, y, w, h, true end
|
||||
local s = math.max(1, math.floor(math.min(w / Playfield.WIDTH,
|
||||
h / Playfield.HEIGHT)))
|
||||
local pw = math.min(w, Playfield.WIDTH * s)
|
||||
local ph = math.min(h, Playfield.HEIGHT * s)
|
||||
return x + math.floor((w - pw) / 2), y + math.floor((h - ph) / 2), pw, ph, true
|
||||
return x, y, w, h, true
|
||||
end
|
||||
|
||||
function Playfield.enter(x, y, w, h)
|
||||
|
||||
@@ -87,12 +87,12 @@ local function displayMetrics()
|
||||
if dpiX < 1e-6 then dpiX = 1 end
|
||||
if dpiY < 1e-6 then dpiY = 1 end
|
||||
local vx, vy = 0, 0
|
||||
local cut, grow = false, false
|
||||
local sx, sy, sw, sh, _, expand = Playfield.cutout(pw, ph)
|
||||
local cut = false
|
||||
local sx, sy, sw, sh = Playfield.cutout(pw, ph)
|
||||
if sx then
|
||||
vx, vy, pw, ph, cut, grow = sx, sy, sw, sh, true, expand
|
||||
vx, vy, pw, ph, cut = sx, sy, sw, sh, true
|
||||
end
|
||||
return ww, wh, pw, ph, dpiX, dpiY, vx, vy, cut, grow
|
||||
return ww, wh, pw, ph, dpiX, dpiY, vx, vy, cut
|
||||
end
|
||||
|
||||
local function positionLift(ph, contentPx, dpiY, cut)
|
||||
@@ -275,7 +275,7 @@ end
|
||||
-- corners; flat mode returns exactly today's size (growth factor is 1 when
|
||||
-- tilt is inactive).
|
||||
function Renderer:worldViewSize()
|
||||
local _, _, pw, ph, _, dpiY, _, _, cut, grow = displayMetrics()
|
||||
local _, _, pw, ph, _, dpiY, _, _, cut = displayMetrics()
|
||||
-- FAITHFUL RATIO on mobile. The world pass deliberately expands to cover the
|
||||
-- WHOLE display, so letterbox voids become more map instead of black bars.
|
||||
-- That is why the lock appeared to do nothing in the overworld: it shrank
|
||||
@@ -287,7 +287,6 @@ function Renderer:worldViewSize()
|
||||
-- this is the same sum with the viewport standing in for the window, so
|
||||
-- both platforms show the same map area at the same zoom.
|
||||
local cap = FaithfulRes.scaleCap()
|
||||
if not cap and cut and not grow then cap = self:fitScale() end
|
||||
if cap then
|
||||
local uiw, uih = self:uiSize()
|
||||
pw = cut and math.min(pw, uiw * cap) or uiw * cap
|
||||
|
||||
@@ -456,9 +456,10 @@ function Studio.setCanvas(index, fromSync)
|
||||
page.viewportFill = false
|
||||
markDirty()
|
||||
end
|
||||
-- A cfg-authored aspect_ratio is the overlay's design aspect; keep it so
|
||||
-- the preview letterboxes like RetroArch instead of stretching (#1503).
|
||||
if page and not page.aspectFromCfg then
|
||||
-- A cfg-authored aspect_ratio is the overlay's design aspect, and bezel art
|
||||
-- is its own design canvas (TouchSkin.applyImageAspect); keep either so the
|
||||
-- preview lays the page out exactly the way the game does (#1503).
|
||||
if page and not (page.aspectFromCfg or page.aspectFromImage) then
|
||||
page.aspect = canvas.w / canvas.h
|
||||
end
|
||||
end
|
||||
|
||||
+16
-5
@@ -749,12 +749,23 @@ function Kit.chip(x, y, w, h, label, on, color, id)
|
||||
end
|
||||
|
||||
-- A status label with no interaction: outlined text, the "INSTALLED"/"UPDATE"
|
||||
-- markers on mod rows.
|
||||
function Kit.tag(x, y, w, h, label, color)
|
||||
-- markers on mod rows. Pass opts.fill for a solid chip (BETA badges); ink
|
||||
-- then defaults to PAL.inverse so the label stays readable on the fill.
|
||||
function Kit.tag(x, y, w, h, label, color, opts)
|
||||
if not G then return end
|
||||
Theme.strokeRounded(x, y, w, h, color or PAL.line, 0.7, 1)
|
||||
Kit.textCenter("micro", label, x, y + (h - Kit.textHeight("micro")) / 2, w,
|
||||
color or PAL.muted)
|
||||
opts = opts or {}
|
||||
if opts.fill then
|
||||
Theme.fillRounded(x, y, w, h, color or PAL.yellow, 1, h / 2)
|
||||
else
|
||||
Theme.strokeRounded(x, y, w, h, color or PAL.line, 0.7, 1)
|
||||
end
|
||||
local ink = opts.ink or (opts.fill and PAL.inverse) or color or PAL.muted
|
||||
local ty = y + (h - Kit.textHeight("micro")) / 2
|
||||
if opts.bold then
|
||||
Kit.textCenterBold("micro", label, x, ty, w, ink)
|
||||
else
|
||||
Kit.textCenter("micro", label, x, ty, w, ink)
|
||||
end
|
||||
end
|
||||
|
||||
-- Checkbox row. Returns (newChecked, changed).
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Screen-rect check for a touch skin, both generations.
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/skin_screen_shot.lua love .
|
||||
-- POKEPORT_GAME=gold POKEPORT_SKIN=my_skin POKEPORT_DRIVER=... love .
|
||||
--
|
||||
-- Gold's cache lives under the DEFAULT save identity, so a POKEPORT_IDENTITY
|
||||
-- sandbox has to be seeded with it first or the boot hangs in the importer:
|
||||
-- cp -R ~/Library/Application\ Support/LOVE/pokemon-love2d/{gold,skins} \
|
||||
-- ~/Library/Application\ Support/LOVE/<identity>/
|
||||
local U = require("tests.drivers.util")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
local function env(name, fallback)
|
||||
local v = os.getenv(name)
|
||||
if v == nil or v == "" then return fallback end
|
||||
return v
|
||||
end
|
||||
|
||||
local function tap(game, button)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
end
|
||||
|
||||
return function(game)
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
local out = env("POKEPORT_SHOT_DIR", ".")
|
||||
local gen2 = GameVersion.generation() == 2
|
||||
|
||||
love.window.setMode(tonumber(env("POKEPORT_SHOT_W", 460)),
|
||||
tonumber(env("POKEPORT_SHOT_H", 950)))
|
||||
local skin = env("POKEPORT_SKIN", "gb_anim")
|
||||
local ok, err = TouchControls:selectSkin(skin)
|
||||
assert(ok, "skin " .. skin .. ": " .. tostring(err))
|
||||
|
||||
U.wait(60)
|
||||
if gen2 then
|
||||
local A = require("tests.drivers.gold.adapter")
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
A.teleport(game, env("POKEPORT_SHOT_MAP", "NEW_BARK_TOWN"), 6, 6)
|
||||
U.wait(120)
|
||||
else
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
game.save.party = { Pokemon.new(game.data, "CHARIZARD", 50) }
|
||||
U.teleport(game, env("POKEPORT_SHOT_MAP", "PALLET_TOWN"), 10, 8, "down")
|
||||
U.wait(40)
|
||||
end
|
||||
U.shot(game, out .. "/skin_world.png")
|
||||
|
||||
tap(game, "start")
|
||||
U.wait(60)
|
||||
U.shot(game, out .. "/skin_menu.png")
|
||||
|
||||
love.event.quit()
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -316,6 +316,8 @@ check(view:find('"tab-sync"', 1, true) ~= nil,
|
||||
local header = view:match("local HEADER_TABS = %{(.-)%}\n")
|
||||
check(header and header:find('id = "skins"', 1, true) ~= nil,
|
||||
"and it sits beside the skins tab")
|
||||
check(header and header:find("beta = true", 1, true) ~= nil,
|
||||
"the skins tab carries a BETA badge too")
|
||||
check(view:find('"BETA"', 1, true) ~= nil,
|
||||
"the button and the modal are labelled BETA")
|
||||
check(view:find("buildSyncModal", 1, true) ~= nil,
|
||||
|
||||
@@ -111,6 +111,25 @@ check(Studio.page().viewport ~= nil, "the SGB cutout cannot be toggled off")
|
||||
Studio.setCanvas(#Studio.CANVASES + 1)
|
||||
eq(Studio.canvasIndex, 1, "canvas selection wraps")
|
||||
|
||||
-- the preview must lay the page out the way the game will, so a preset never
|
||||
-- overwrites the aspect the bezel art (or a cfg) already fixed
|
||||
session()
|
||||
local art = Studio.page()
|
||||
art.aspect, art.aspectFromImage = 0.5625, true
|
||||
Studio.setCanvas(1)
|
||||
near(art.aspect, 0.5625, 1e-9, "bezel art keeps its own aspect on a preset switch")
|
||||
local deckW, deckH = 900, 1700
|
||||
local _, dby, _, dbh = TouchSkin.pageBox(art, deckW, deckH)
|
||||
check(dby > 0 and math.abs(dby + dbh - deckH) < 1e-6,
|
||||
"so a preset taller than the art pins the deck low, as gameplay does")
|
||||
|
||||
session()
|
||||
local plain = Studio.page()
|
||||
plain.aspect, plain.aspectFromImage, plain.aspectFromCfg = nil, nil, nil
|
||||
Studio.setCanvas(1)
|
||||
near(plain.aspect, Studio.canvas().w / Studio.canvas().h, 1e-9,
|
||||
"a page with no art of its own still takes the preset's aspect")
|
||||
|
||||
Studio.setCanvas(1)
|
||||
Studio.viewZoom = 1
|
||||
local cx, cy, cw, ch = Studio.canvasRect(0, 0, 800, 600)
|
||||
|
||||
@@ -136,11 +136,16 @@ for off = lo, hi do
|
||||
end
|
||||
Zoom.offset = 0
|
||||
|
||||
local capped = select(1, Renderer:worldViewSize())
|
||||
useSkin("0.25,0.1,0.5,0.6", "overlay0_viewport_expand = true")
|
||||
local expanded = select(1, Renderer:worldViewSize())
|
||||
check(expanded > capped,
|
||||
"viewport_expand lets the survey world fill the cutout instead of the GB box")
|
||||
useSkin("0.25,0.1,0.3,0.8")
|
||||
local tallR = Renderer:frameRects()
|
||||
local tallVw, tallVh = Renderer:worldViewSize()
|
||||
local tallSp = Zoom.scale(tallR.Sp)
|
||||
check(tallVw * tallSp >= tallR.vuw and tallVh * tallSp >= tallR.vuh,
|
||||
"the world pass covers the whole cutout, not just the GB box inside it")
|
||||
check(tallVh * tallSp > tallR.uvph,
|
||||
"so a cutout taller than 160x144 shows map where the UI letterbox ends")
|
||||
check(tallR.uvph < tallR.vuh and tallR.uvpw <= tallR.vuw,
|
||||
"while the UI keeps its whole-pixel letterbox inside that cutout")
|
||||
useSkin("0.25,0.1,0.5,0.6")
|
||||
|
||||
setWindow(480, 800)
|
||||
@@ -159,22 +164,14 @@ setWindow(1280, 720)
|
||||
useSkin("0.25,0.1,0.5,0.6")
|
||||
local px, py, pw, ph, active = Playfield.rect(1280, 720)
|
||||
eq(active, true, "Gold sees the cutout too")
|
||||
eq(pw, 480, "the playfield is a whole multiple of 160")
|
||||
eq(ph, 432, "and of 144")
|
||||
check(inside(px, py, pw, ph, 320, 72, 640, 432),
|
||||
"centred inside the cutout")
|
||||
eq(Chrome.fitScale(1280, 720), 3, "Chrome fits the playfield")
|
||||
eq(px, 320, "the playfield is the cutout, at its origin")
|
||||
eq(py, 72, "on both axes")
|
||||
eq(pw, 640, "with its full width")
|
||||
eq(ph, 432, "and its full height")
|
||||
eq(Chrome.fitScale(1280, 720), 3, "Chrome fits whole GB pixels in it")
|
||||
local cox, coy = Chrome.fitOrigin(1280, 720)
|
||||
eq(cox, px, "and centres the panel on it")
|
||||
eq(coy, py, "on both axes")
|
||||
|
||||
useSkin("0.25,0.1,0.5,0.6", "overlay0_viewport_expand = true")
|
||||
local ex, ey, ew, eh = Playfield.rect(1280, 720)
|
||||
eq(ew, 640, "expand hands the picture the full cutout width")
|
||||
eq(eh, 432, "and its full height")
|
||||
eq(ex, 320, "at the cutout origin")
|
||||
eq(ey, 72, "on both axes")
|
||||
useSkin("0.25,0.1,0.5,0.6")
|
||||
eq(cox, 320 + 80, "and centres the panel on the cutout")
|
||||
eq(coy, 72, "on both axes")
|
||||
|
||||
local ew2, eh2, ex2, ey2, act2 = Playfield.push(1280, 720)
|
||||
eq(act2, true, "push reports the frame is contained")
|
||||
@@ -184,12 +181,29 @@ eq(ew2, pw, "and hands the scene the playfield size")
|
||||
eq(Playfield.cutout(ew2, eh2), nil, "inside the frame there is no cutout left")
|
||||
eq(select(3, Playfield.rect(ew2, eh2)), pw, "so the playfield is the surface")
|
||||
eq(Chrome.fitScale(ew2, eh2), 3, "and Chrome fits it without re-applying")
|
||||
eq(select(1, Chrome.fitOrigin(ew2, eh2)), 0, "at a local origin")
|
||||
eq(select(1, Chrome.fitOrigin(ew2, eh2)), 80, "centred on that surface alone")
|
||||
eq(select(1, Playfield.dimensions()), pw, "screens read the playfield as the display")
|
||||
Playfield.pop()
|
||||
eq(Playfield.entered, false, "pop leaves the frame")
|
||||
eq(select(1, Playfield.cutout(1280, 720)), 320, "and the cutout is visible again")
|
||||
|
||||
setWindow(960, 1901)
|
||||
useSkin("0,0,1,0.5", "overlay0_aspect_ratio = 0.5625")
|
||||
local deck = TouchSkin.page()
|
||||
local _, dy, _, dh = TouchSkin.pageBox(deck, 960, 1901)
|
||||
check(dy > 0 and math.abs(dy + dh - 1901) < 1,
|
||||
"a portrait deck taller than the display pins to the lower edge")
|
||||
local hx, hy, hw, hh = Playfield.cutout(960, 1901)
|
||||
eq(hy, 0, "and the screen rect it left flush takes the room above it")
|
||||
eq(hx, 0, "without moving sideways")
|
||||
eq(hw, 960, "or changing width")
|
||||
eq(hh, math.floor(dy + dh * 0.5), "growing by exactly the headroom")
|
||||
|
||||
useSkin("0,0.1,1,0.5", "overlay0_aspect_ratio = 0.5625")
|
||||
check(select(2, Playfield.cutout(960, 1901)) > dy,
|
||||
"a screen rect the author inset from the top keeps that bezel margin")
|
||||
|
||||
setWindow(1280, 720)
|
||||
useSkin("0.4,0.4,0.1,0.1")
|
||||
local sx, sy, sw, sh = Playfield.rect(1280, 720)
|
||||
check(inside(sx, sy, sw, sh, 512, 288, 128, 72),
|
||||
|
||||
Reference in New Issue
Block a user