This commit is contained in:
bryanthaboi
2026-08-21 16:36:55 -04:00
parent 06e06e305b
commit 087a275189
10 changed files with 171 additions and 57 deletions
+13 -6
View File
@@ -294,7 +294,6 @@ function TouchSkin.parse(text)
page.viewport = { x = num(vp[1], 0), y = num(vp[2], 0), page.viewport = { x = num(vp[1], 0), y = num(vp[2], 0),
w = num(vp[3], 1), h = num(vp[4], 1) } w = num(vp[3], 1), h = num(vp[4], 1) }
page.viewportFill = toBool(kv[p .. "_viewport_fill"]) page.viewportFill = toBool(kv[p .. "_viewport_fill"])
page.viewportExpand = toBool(kv[p .. "_viewport_expand"])
end end
page.pixelCoords = not page.normalized 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), 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) } w = num(raw.viewport.w, 1), h = num(raw.viewport.h, 1) }
page.viewportFill = raw.viewport.fill == true page.viewportFill = raw.viewport.fill == true
page.viewportExpand = raw.viewport.expand == true
end end
for _, c in ipairs(raw.controls or {}) do for _, c in ipairs(raw.controls or {}) do
local buttons, hotkeys, keys, decorative = parseBinds(c.bind or "nul") 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, x = page.viewport.x, y = page.viewport.y,
w = page.viewport.w, h = page.viewport.h, w = page.viewport.w, h = page.viewport.h,
fill = page.viewportFill or nil, fill = page.viewportFill or nil,
expand = page.viewportExpand or nil,
} }
end end
for _, ctl in ipairs(page.controls or {}) do for _, ctl in ipairs(page.controls or {}) do
@@ -922,7 +919,6 @@ function TouchSkin.toRetroArchConfig(skin)
if page.viewport then if page.viewport then
out[#out + 1] = p .. "_viewport = " .. fmtRect(page.viewport) out[#out + 1] = p .. "_viewport = " .. fmtRect(page.viewport)
if page.viewportFill then out[#out + 1] = p .. "_viewport_fill = true" end 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 end
local controls = {} local controls = {}
for _, ctl in ipairs(page.controls or {}) do 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] return best[1], best[2], best[3], best[4]
end 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) function TouchSkin.pageViewport(page, w, h, ox, oy)
if not page then return nil end if not page then return nil end
ox, oy = ox or 0, oy or 0 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 x, y = bx + v.x * bw, by + v.y * bh
local vw, vh = v.w * bw, v.h * bh local vw, vh = v.w * bw, v.h * bh
if vw <= 0 or vh <= 0 then return nil end 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 end
if page.screenFit == "remainder" then if page.screenFit == "remainder" then
local x, y, vw, vh = remainderBox(ox, oy, w, h, bx, by, bw, bh) local x, y, vw, vh = remainderBox(ox, oy, w, h, bx, by, bw, bh)
if not x then return nil end if not x then return nil end
return x, y, vw, vh, false, false return x, y, vw, vh, false
end end
return nil return nil
end end
+18 -7
View File
@@ -1067,9 +1067,22 @@ local GAME_TABS = {
local HEADER_TABS = { local HEADER_TABS = {
{ id = "mods", key = "tab-mods" }, { id = "mods", key = "tab-mods" },
{ id = "find", key = "tab-find" }, { 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" }, { 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 for _, t in ipairs(HEADER_TABS) do
t.opts = { face = "tab", font = "tab", color = t.color, letter = t.letter } t.opts = { face = "tab", font = "tab", color = t.color, letter = t.letter }
if t.glyph then if t.glyph then
@@ -1273,6 +1286,7 @@ local function buildHeader(imp, m)
o.image = t.icon o.image = t.icon
o.action = chrome.tab[t.id] o.action = chrome.tab[t.id]
btn(imp, tx, ty, w, tabH, t.key, "", o) btn(imp, tx, ty, w, tabH, t.key, "", o)
if t.beta then overlayBeta(tx, ty, w, tabH, m) end
tx = tx + w + tabGap tx = tx + w + tabGap
end end
-- The bug-report chip sits LAST, past the sync chip. -- The bug-report chip sits LAST, past the sync chip.
@@ -1290,10 +1304,7 @@ local function buildHeader(imp, m)
local o = chrome.sync local o = chrome.sync
o.active = imp._syncModal ~= nil o.active = imp._syncModal ~= nil
btn(imp, tx, ty, w, tabH, "tab-sync", "", o) btn(imp, tx, ty, w, tabH, "tab-sync", "", o)
local bh = math.floor(11 * m.s) overlayBeta(tx, ty, w, tabH, m)
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)
local eng = imp._sync local eng = imp._sync
if eng and eng.busy and eng:busy() then if eng and eng.busy and eng:busy() then
Kit.spinner(tx + w - math.floor(8 * m.s), ty + math.floor(8 * m.s), 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) Kit.text("button", label, px + pad, py, PAL.heading)
local bh = math.floor(15 * m.s) local bh = math.floor(15 * m.s)
local bw = Kit.textWidth("micro", "BETA") + math.floor(14 * 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), drawBetaTag(px + pad + Kit.textWidth("button", label) + math.floor(8 * m.s),
py + (Kit.textHeight("button") - bh) / 2, bw, bh, "BETA", PAL.yellow) py + (Kit.textHeight("button") - bh) / 2, bw, bh)
return py + Kit.textHeight("button") + math.floor(12 * m.s) return py + Kit.textHeight("button") + math.floor(12 * m.s)
end end
+2 -9
View File
@@ -3,8 +3,6 @@ local TouchSkin = require("src.core.TouchSkin")
local Playfield = {} local Playfield = {}
Playfield.WIDTH, Playfield.HEIGHT = 160, 144
Playfield.entered = false Playfield.entered = false
Playfield.box = nil Playfield.box = nil
@@ -35,14 +33,9 @@ function Playfield.cutout(sw, sh)
end end
function Playfield.rect(sw, sh) 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 not x then return 0, 0, sw or 0, sh or 0, false end
if expand then return x, y, w, h, true end return x, y, w, h, true
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
end end
function Playfield.enter(x, y, w, h) function Playfield.enter(x, y, w, h)
+5 -6
View File
@@ -87,12 +87,12 @@ local function displayMetrics()
if dpiX < 1e-6 then dpiX = 1 end if dpiX < 1e-6 then dpiX = 1 end
if dpiY < 1e-6 then dpiY = 1 end if dpiY < 1e-6 then dpiY = 1 end
local vx, vy = 0, 0 local vx, vy = 0, 0
local cut, grow = false, false local cut = false
local sx, sy, sw, sh, _, expand = Playfield.cutout(pw, ph) local sx, sy, sw, sh = Playfield.cutout(pw, ph)
if sx then 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 end
return ww, wh, pw, ph, dpiX, dpiY, vx, vy, cut, grow return ww, wh, pw, ph, dpiX, dpiY, vx, vy, cut
end end
local function positionLift(ph, contentPx, dpiY, cut) 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 -- corners; flat mode returns exactly today's size (growth factor is 1 when
-- tilt is inactive). -- tilt is inactive).
function Renderer:worldViewSize() 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 -- FAITHFUL RATIO on mobile. The world pass deliberately expands to cover the
-- WHOLE display, so letterbox voids become more map instead of black bars. -- 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 -- 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 -- 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. -- both platforms show the same map area at the same zoom.
local cap = FaithfulRes.scaleCap() local cap = FaithfulRes.scaleCap()
if not cap and cut and not grow then cap = self:fitScale() end
if cap then if cap then
local uiw, uih = self:uiSize() local uiw, uih = self:uiSize()
pw = cut and math.min(pw, uiw * cap) or uiw * cap pw = cut and math.min(pw, uiw * cap) or uiw * cap
+4 -3
View File
@@ -456,9 +456,10 @@ function Studio.setCanvas(index, fromSync)
page.viewportFill = false page.viewportFill = false
markDirty() markDirty()
end end
-- A cfg-authored aspect_ratio is the overlay's design aspect; keep it so -- A cfg-authored aspect_ratio is the overlay's design aspect, and bezel art
-- the preview letterboxes like RetroArch instead of stretching (#1503). -- is its own design canvas (TouchSkin.applyImageAspect); keep either so the
if page and not page.aspectFromCfg then -- 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 page.aspect = canvas.w / canvas.h
end end
end end
+16 -5
View File
@@ -749,12 +749,23 @@ function Kit.chip(x, y, w, h, label, on, color, id)
end end
-- A status label with no interaction: outlined text, the "INSTALLED"/"UPDATE" -- A status label with no interaction: outlined text, the "INSTALLED"/"UPDATE"
-- markers on mod rows. -- markers on mod rows. Pass opts.fill for a solid chip (BETA badges); ink
function Kit.tag(x, y, w, h, label, color) -- 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 if not G then return end
Theme.strokeRounded(x, y, w, h, color or PAL.line, 0.7, 1) opts = opts or {}
Kit.textCenter("micro", label, x, y + (h - Kit.textHeight("micro")) / 2, w, if opts.fill then
color or PAL.muted) 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 end
-- Checkbox row. Returns (newChecked, changed). -- Checkbox row. Returns (newChecked, changed).
+57
View File
@@ -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
+2
View File
@@ -316,6 +316,8 @@ check(view:find('"tab-sync"', 1, true) ~= nil,
local header = view:match("local HEADER_TABS = %{(.-)%}\n") local header = view:match("local HEADER_TABS = %{(.-)%}\n")
check(header and header:find('id = "skins"', 1, true) ~= nil, check(header and header:find('id = "skins"', 1, true) ~= nil,
"and it sits beside the skins tab") "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, check(view:find('"BETA"', 1, true) ~= nil,
"the button and the modal are labelled BETA") "the button and the modal are labelled BETA")
check(view:find("buildSyncModal", 1, true) ~= nil, check(view:find("buildSyncModal", 1, true) ~= nil,
+19
View File
@@ -111,6 +111,25 @@ check(Studio.page().viewport ~= nil, "the SGB cutout cannot be toggled off")
Studio.setCanvas(#Studio.CANVASES + 1) Studio.setCanvas(#Studio.CANVASES + 1)
eq(Studio.canvasIndex, 1, "canvas selection wraps") 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.setCanvas(1)
Studio.viewZoom = 1 Studio.viewZoom = 1
local cx, cy, cw, ch = Studio.canvasRect(0, 0, 800, 600) local cx, cy, cw, ch = Studio.canvasRect(0, 0, 800, 600)
+35 -21
View File
@@ -136,11 +136,16 @@ for off = lo, hi do
end end
Zoom.offset = 0 Zoom.offset = 0
local capped = select(1, Renderer:worldViewSize()) useSkin("0.25,0.1,0.3,0.8")
useSkin("0.25,0.1,0.5,0.6", "overlay0_viewport_expand = true") local tallR = Renderer:frameRects()
local expanded = select(1, Renderer:worldViewSize()) local tallVw, tallVh = Renderer:worldViewSize()
check(expanded > capped, local tallSp = Zoom.scale(tallR.Sp)
"viewport_expand lets the survey world fill the cutout instead of the GB box") 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") useSkin("0.25,0.1,0.5,0.6")
setWindow(480, 800) setWindow(480, 800)
@@ -159,22 +164,14 @@ setWindow(1280, 720)
useSkin("0.25,0.1,0.5,0.6") useSkin("0.25,0.1,0.5,0.6")
local px, py, pw, ph, active = Playfield.rect(1280, 720) local px, py, pw, ph, active = Playfield.rect(1280, 720)
eq(active, true, "Gold sees the cutout too") eq(active, true, "Gold sees the cutout too")
eq(pw, 480, "the playfield is a whole multiple of 160") eq(px, 320, "the playfield is the cutout, at its origin")
eq(ph, 432, "and of 144") eq(py, 72, "on both axes")
check(inside(px, py, pw, ph, 320, 72, 640, 432), eq(pw, 640, "with its full width")
"centred inside the cutout") eq(ph, 432, "and its full height")
eq(Chrome.fitScale(1280, 720), 3, "Chrome fits the playfield") eq(Chrome.fitScale(1280, 720), 3, "Chrome fits whole GB pixels in it")
local cox, coy = Chrome.fitOrigin(1280, 720) local cox, coy = Chrome.fitOrigin(1280, 720)
eq(cox, px, "and centres the panel on it") eq(cox, 320 + 80, "and centres the panel on the cutout")
eq(coy, py, "on both axes") eq(coy, 72, "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")
local ew2, eh2, ex2, ey2, act2 = Playfield.push(1280, 720) local ew2, eh2, ex2, ey2, act2 = Playfield.push(1280, 720)
eq(act2, true, "push reports the frame is contained") 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(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(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(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") eq(select(1, Playfield.dimensions()), pw, "screens read the playfield as the display")
Playfield.pop() Playfield.pop()
eq(Playfield.entered, false, "pop leaves the frame") eq(Playfield.entered, false, "pop leaves the frame")
eq(select(1, Playfield.cutout(1280, 720)), 320, "and the cutout is visible again") 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") useSkin("0.4,0.4,0.1,0.1")
local sx, sy, sw, sh = Playfield.rect(1280, 720) local sx, sy, sw, sh = Playfield.rect(1280, 720)
check(inside(sx, sy, sw, sh, 512, 288, 128, 72), check(inside(sx, sy, sw, sh, 512, 288, 128, 72),