mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
CLOSES #1603
This commit is contained in:
@@ -11,6 +11,7 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
|||||||
* **Persistent custom options** stored separately from game saves
|
* **Persistent custom options** stored separately from game saves
|
||||||
* **Optional widescreen battle layout**
|
* **Optional widescreen battle layout**
|
||||||
* **Mobile touch controls** with editable layouts, vibration, and orientation settings
|
* **Mobile touch controls** with editable layouts, vibration, and orientation settings
|
||||||
|
* **Screen position setting** (center, upper, top) shared across all games, for clamp-on controllers that cover the lower screen
|
||||||
* **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders
|
* **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders
|
||||||
* **Pokédex diploma and printer image exports**
|
* **Pokédex diploma and printer image exports**
|
||||||
|
|
||||||
|
|||||||
@@ -1260,6 +1260,7 @@ function Game:applyOptions(opts)
|
|||||||
-- after VideoMode: a faithful-resolution lock is an exact window size, so
|
-- after VideoMode: a faithful-resolution lock is an exact window size, so
|
||||||
-- it has to be the last word on the window (it drops fullscreen to hold)
|
-- it has to be the last word on the window (it drops fullscreen to hold)
|
||||||
require("src.core.FaithfulRes").applyOptions(opts)
|
require("src.core.FaithfulRes").applyOptions(opts)
|
||||||
|
require("src.core.ScreenPosition").applyOptions(opts)
|
||||||
-- normalizes a nil/garbage cap to the 60 default, so old saves with no
|
-- normalizes a nil/garbage cap to the 60 default, so old saves with no
|
||||||
-- fpsCap key pace at the standard rate (issue #88)
|
-- fpsCap key pace at the standard rate (issue #88)
|
||||||
require("src.core.FrameCap").applyOptions(opts)
|
require("src.core.FrameCap").applyOptions(opts)
|
||||||
|
|||||||
+3
-1
@@ -1660,8 +1660,9 @@ function Game2:drawScene(w, h)
|
|||||||
-- row to opt into the step-down half, so CENTERED is the whole rule
|
-- row to opt into the step-down half, so CENTERED is the whole rule
|
||||||
-- here.
|
-- here.
|
||||||
local s = self.world:fitScale()
|
local s = self.world:fitScale()
|
||||||
|
local ox, oy = Chrome.fitOrigin(w, h, s)
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(math.floor((w - 160 * s) / 2), math.floor((h - 144 * s) / 2))
|
G.translate(ox, oy)
|
||||||
G.scale(s, s)
|
G.scale(s, s)
|
||||||
self.stack:draw()
|
self.stack:draw()
|
||||||
G.pop()
|
G.pop()
|
||||||
@@ -1982,6 +1983,7 @@ function Game2:applyOptions()
|
|||||||
haptics = options.haptics,
|
haptics = options.haptics,
|
||||||
})
|
})
|
||||||
require("src.core.VideoMode").applyOptions(options)
|
require("src.core.VideoMode").applyOptions(options)
|
||||||
|
require("src.core.ScreenPosition").applyOptions(options)
|
||||||
require("src.core.FrameCap").applyOptions(options)
|
require("src.core.FrameCap").applyOptions(options)
|
||||||
require("src.world.gen2.BorderFill").applyOptions(options)
|
require("src.world.gen2.BorderFill").applyOptions(options)
|
||||||
local GBCFX = require("src.render.GBCFX")
|
local GBCFX = require("src.render.GBCFX")
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ function SaveData.defaultOptions()
|
|||||||
-- lock the window to an exact 160x144 multiple, 1..4 (0 = OFF); see
|
-- lock the window to an exact 160x144 multiple, 1..4 (0 = OFF); see
|
||||||
-- src/core/FaithfulRes.lua. Ignored on mobile.
|
-- src/core/FaithfulRes.lua. Ignored on mobile.
|
||||||
faithfulRes = 0,
|
faithfulRes = 0,
|
||||||
|
screenPos = "center",
|
||||||
-- hard render frame-rate cap; render-only pacing (issue #88, FrameCap.lua)
|
-- hard render frame-rate cap; render-only pacing (issue #88, FrameCap.lua)
|
||||||
fpsCap = 60,
|
fpsCap = 60,
|
||||||
-- graphics performance tier: auto | high | balanced | low. "auto"
|
-- graphics performance tier: auto | high | balanced | low. "auto"
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
local ScreenPosition = {}
|
||||||
|
|
||||||
|
ScreenPosition.MODES = { "center", "upper", "top" }
|
||||||
|
ScreenPosition.DEFAULT = "center"
|
||||||
|
ScreenPosition.mode = ScreenPosition.DEFAULT
|
||||||
|
|
||||||
|
local LABELS = { center = "CENTER", upper = "UPPER", top = "TOP" }
|
||||||
|
|
||||||
|
function ScreenPosition.normalize(v)
|
||||||
|
if LABELS[v] then return v end
|
||||||
|
return ScreenPosition.DEFAULT
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.label(v)
|
||||||
|
return LABELS[ScreenPosition.normalize(v)]
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.cycle(v, dir)
|
||||||
|
v = ScreenPosition.normalize(v)
|
||||||
|
local modes = ScreenPosition.MODES
|
||||||
|
local cur = 1
|
||||||
|
for i, mode in ipairs(modes) do
|
||||||
|
if mode == v then cur = i break end
|
||||||
|
end
|
||||||
|
return modes[(cur - 1 + (dir or 1)) % #modes + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.setMode(v)
|
||||||
|
ScreenPosition.mode = ScreenPosition.normalize(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.applyOptions(opts)
|
||||||
|
ScreenPosition.setMode(opts and opts.screenPos)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.safeTop()
|
||||||
|
local ok, SafeArea = pcall(require, "src.core.SafeArea")
|
||||||
|
if not ok then return 0 end
|
||||||
|
local okr, _, y = pcall(SafeArea.rect)
|
||||||
|
if not okr then return 0 end
|
||||||
|
return math.max(0, tonumber(y) or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.skinActive(w, h)
|
||||||
|
local ok, TouchSkin = pcall(require, "src.core.TouchSkin")
|
||||||
|
if not ok or type(TouchSkin.viewport) ~= "function" then return false end
|
||||||
|
local okv, x = pcall(TouchSkin.viewport, w, h)
|
||||||
|
return okv and x ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScreenPosition.lift(viewH, contentH, safeTop)
|
||||||
|
if ScreenPosition.mode == "center" then return 0 end
|
||||||
|
viewH = tonumber(viewH) or 0
|
||||||
|
contentH = tonumber(contentH) or 0
|
||||||
|
local slack = viewH - contentH
|
||||||
|
if slack <= 0 then return 0 end
|
||||||
|
local centered = math.floor(slack / 2)
|
||||||
|
local target = ScreenPosition.mode == "top" and 0 or math.floor(slack / 4)
|
||||||
|
safeTop = math.floor(tonumber(safeTop) or 0)
|
||||||
|
if safeTop > 0 and target < safeTop then
|
||||||
|
target = math.min(safeTop, centered)
|
||||||
|
end
|
||||||
|
return centered - target
|
||||||
|
end
|
||||||
|
|
||||||
|
return ScreenPosition
|
||||||
@@ -288,6 +288,7 @@ Save.DEFAULT_OPTIONS = {
|
|||||||
musicFilter = 0, -- low-pass steps, 0 = off
|
musicFilter = 0, -- low-pass steps, 0 = off
|
||||||
haptics = "light",
|
haptics = "light",
|
||||||
touchControls = { enabled = true },
|
touchControls = { enabled = true },
|
||||||
|
screenPos = "center",
|
||||||
}
|
}
|
||||||
|
|
||||||
function Save.defaultOptions()
|
function Save.defaultOptions()
|
||||||
@@ -308,7 +309,7 @@ end
|
|||||||
Save.OPTIONS_KEY = "gold"
|
Save.OPTIONS_KEY = "gold"
|
||||||
|
|
||||||
local SHARED_KEYS = {
|
local SHARED_KEYS = {
|
||||||
touchControls = true, haptics = true,
|
touchControls = true, haptics = true, screenPos = true,
|
||||||
mods = true, modsByVersion = true, modsGen2 = true,
|
mods = true, modsByVersion = true, modsGen2 = true,
|
||||||
modOptions = true, modProfiles = true, modProfilesSeeded = true,
|
modOptions = true, modProfiles = true, modProfilesSeeded = true,
|
||||||
activeProfile = true,
|
activeProfile = true,
|
||||||
|
|||||||
@@ -332,6 +332,17 @@ local function coreRows(opts, hooks)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local okSp, ScreenPos = pcall(require, "src.core.ScreenPosition")
|
||||||
|
if okSp then
|
||||||
|
add(Strings("SCREEN POS"),
|
||||||
|
function() return Strings(ScreenPos.label(opts.screenPos)) end,
|
||||||
|
function(dir)
|
||||||
|
opts.screenPos = ScreenPos.cycle(opts.screenPos, dir)
|
||||||
|
ScreenPos.setMode(opts.screenPos)
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local okCap, FrameCap = pcall(require, "src.core.FrameCap")
|
local okCap, FrameCap = pcall(require, "src.core.FrameCap")
|
||||||
if okCap then
|
if okCap then
|
||||||
add(Strings("MAX FPS"),
|
add(Strings("MAX FPS"),
|
||||||
|
|||||||
+14
-4
@@ -15,6 +15,7 @@ local Runtime = require("src.mods.Runtime")
|
|||||||
local GameViewport = require("src.render.GameViewport")
|
local GameViewport = require("src.render.GameViewport")
|
||||||
-- leaf module (no renderer dependency), so requiring it here cannot cycle
|
-- leaf module (no renderer dependency), so requiring it here cannot cycle
|
||||||
local FaithfulRes = require("src.core.FaithfulRes")
|
local FaithfulRes = require("src.core.FaithfulRes")
|
||||||
|
local ScreenPosition = require("src.core.ScreenPosition")
|
||||||
local Playfield = require("src.render.Playfield")
|
local Playfield = require("src.render.Playfield")
|
||||||
|
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
@@ -94,6 +95,11 @@ local function displayMetrics()
|
|||||||
return ww, wh, pw, ph, dpiX, dpiY, vx, vy, cut, grow
|
return ww, wh, pw, ph, dpiX, dpiY, vx, vy, cut, grow
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function positionLift(ph, contentPx, dpiY, cut)
|
||||||
|
if cut then return 0 end
|
||||||
|
return ScreenPosition.lift(ph, contentPx, ScreenPosition.safeTop() * dpiY)
|
||||||
|
end
|
||||||
|
|
||||||
function Renderer:init()
|
function Renderer:init()
|
||||||
-- 160x144 real pixels, never DPI-scaled: see src/render/PixelCanvas.lua
|
-- 160x144 real pixels, never DPI-scaled: see src/render/PixelCanvas.lua
|
||||||
-- (#208). Every canvas below is sized in framebuffer pixels for the same
|
-- (#208). Every canvas below is sized in framebuffer pixels for the same
|
||||||
@@ -269,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, _, _, _, _, cut, grow = displayMetrics()
|
local _, _, pw, ph, _, dpiY, _, _, cut, grow = 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
|
||||||
@@ -293,6 +299,9 @@ function Renderer:worldViewSize()
|
|||||||
-- so unfloored FX/sprite math cannot phase-shimmer against the tile layer.
|
-- so unfloored FX/sprite math cannot phase-shimmer against the tile layer.
|
||||||
if vw % 2 ~= 0 then vw = vw + 1 end
|
if vw % 2 ~= 0 then vw = vw + 1 end
|
||||||
if vh % 2 ~= 0 then vh = vh + 1 end
|
if vh % 2 ~= 0 then vh = vh + 1 end
|
||||||
|
local _, uih = self:uiSize()
|
||||||
|
local lift = positionLift(ph, uih * self:fitScale(), dpiY, cut)
|
||||||
|
if lift > 0 then vh = vh + 2 * math.ceil(lift / sp) end
|
||||||
if Tilt.active() then
|
if Tilt.active() then
|
||||||
local g = Tilt.viewGrowth()
|
local g = Tilt.viewGrowth()
|
||||||
vw, vh = math.ceil(vw * g), math.ceil(vh * g)
|
vw, vh = math.ceil(vw * g), math.ceil(vh * g)
|
||||||
@@ -774,8 +783,9 @@ function Renderer:frameRects()
|
|||||||
r.uiw, r.uih = uiw, uih
|
r.uiw, r.uih = uiw, uih
|
||||||
r.vpw, r.vph = uiw * r.Sx, uih * r.Sy
|
r.vpw, r.vph = uiw * r.Sx, uih * r.Sy
|
||||||
-- Snap the letterbox origin to a framebuffer pixel, then convert to units.
|
-- Snap the letterbox origin to a framebuffer pixel, then convert to units.
|
||||||
|
r.lift = positionLift(ph, uih * Sp, dpiY, cut)
|
||||||
r.ox = (vx + math.floor((pw - uiw * Sp) / 2)) / dpiX
|
r.ox = (vx + math.floor((pw - uiw * Sp) / 2)) / dpiX
|
||||||
r.oy = (vy + math.floor((ph - uih * Sp) / 2)) / dpiY
|
r.oy = (vy + math.floor((ph - uih * Sp) / 2) - r.lift) / dpiY
|
||||||
-- The UI has its own scale: it steps down as the survey zoom goes out (see
|
-- The UI has its own scale: it steps down as the survey zoom goes out (see
|
||||||
-- uiScale), so it can be smaller than the world letterbox. Un-zoomed these
|
-- uiScale), so it can be smaller than the world letterbox. Un-zoomed these
|
||||||
-- are identical to Sp/ox/oy and every rect below is what it always was.
|
-- are identical to Sp/ox/oy and every rect below is what it always was.
|
||||||
@@ -795,7 +805,7 @@ function Renderer:frameRects()
|
|||||||
r.Up, r.Ux, r.Uy = Up, Up / dpiX, Up / dpiY
|
r.Up, r.Ux, r.Uy = Up, Up / dpiX, Up / dpiY
|
||||||
r.uvpw, r.uvph = uiw * r.Ux, uih * r.Uy
|
r.uvpw, r.uvph = uiw * r.Ux, uih * r.Uy
|
||||||
r.uox = (vx + math.floor((pw - uiw * Up) / 2)) / dpiX
|
r.uox = (vx + math.floor((pw - uiw * Up) / 2)) / dpiX
|
||||||
r.uoy = (vy + math.floor((ph - uih * Up) / 2)) / dpiY
|
r.uoy = (vy + math.max(0, math.floor((ph - uih * Up) / 2) - r.lift)) / dpiY
|
||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1002,7 +1012,7 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
local wvw = self.worldCanvas:getWidth()
|
local wvw = self.worldCanvas:getWidth()
|
||||||
local wvh = self.worldCanvas:getHeight()
|
local wvh = self.worldCanvas:getHeight()
|
||||||
local wox = (vx + math.floor((pw - wvw * sp) / 2)) / dpiX
|
local wox = (vx + math.floor((pw - wvw * sp) / 2)) / dpiX
|
||||||
local woy = (vy + math.floor((ph - wvh * sp) / 2)) / dpiY
|
local woy = (vy + math.floor((ph - wvh * sp) / 2) - R.lift) / dpiY
|
||||||
-- Tilt mode projects the ground world pass through the perspective mesh
|
-- Tilt mode projects the ground world pass through the perspective mesh
|
||||||
-- (SGB zones baked in beforehand -- see drawTiltedWorld -- so no zone
|
-- (SGB zones baked in beforehand -- see drawTiltedWorld -- so no zone
|
||||||
-- scissoring here). drawTiltedWorld returns false when tilt is off or
|
-- scissoring here). drawTiltedWorld returns false when tilt is off or
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ local GameVersion = require("src.core.GameVersion")
|
|||||||
local VideoMode = require("src.core.VideoMode")
|
local VideoMode = require("src.core.VideoMode")
|
||||||
local Orientation = require("src.core.Orientation")
|
local Orientation = require("src.core.Orientation")
|
||||||
local FaithfulRes = require("src.core.FaithfulRes")
|
local FaithfulRes = require("src.core.FaithfulRes")
|
||||||
|
local ScreenPosition = require("src.core.ScreenPosition")
|
||||||
local FrameCap = require("src.core.FrameCap")
|
local FrameCap = require("src.core.FrameCap")
|
||||||
local Performance = require("src.core.Performance")
|
local Performance = require("src.core.Performance")
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
@@ -443,6 +444,16 @@ local function buildRows(game)
|
|||||||
FaithfulRes.apply(o.faithfulRes)
|
FaithfulRes.apply(o.faithfulRes)
|
||||||
return true
|
return true
|
||||||
end },
|
end },
|
||||||
|
{ id = "screenPos", label = Strings("SCREEN POS"),
|
||||||
|
value = function(g)
|
||||||
|
return Strings(ScreenPosition.label(g.save.options.screenPos))
|
||||||
|
end,
|
||||||
|
step = function(g, dir)
|
||||||
|
local o = g.save.options
|
||||||
|
o.screenPos = ScreenPosition.cycle(o.screenPos, dir)
|
||||||
|
ScreenPosition.setMode(o.screenPos)
|
||||||
|
return true
|
||||||
|
end },
|
||||||
-- hard render cap (issue #88): bounds the present rate so a
|
-- hard render cap (issue #88): bounds the present rate so a
|
||||||
-- driver-forced vsync-off run cannot spin at thousands of FPS. Logic
|
-- driver-forced vsync-off run cannot spin at thousands of FPS. Logic
|
||||||
-- is fixed-step off dt, so this touches presentation only.
|
-- is fixed-step off dt, so this touches presentation only.
|
||||||
|
|||||||
@@ -619,8 +619,8 @@ function BattleTransition:grid(w, h)
|
|||||||
scale = math.max(1, math.floor(math.min(w / 160, h / 144)))
|
scale = math.max(1, math.floor(math.min(w / 160, h / 144)))
|
||||||
end
|
end
|
||||||
local size = 8 * scale
|
local size = 8 * scale
|
||||||
local ox = math.floor((w - 160 * scale) / 2)
|
local Chrome = require("src.ui.gen2.Chrome")
|
||||||
local oy = math.floor((h - 144 * scale) / 2)
|
local ox, oy = Chrome.fitOrigin(w, h, scale)
|
||||||
return size, ox, oy
|
return size, ox, oy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ function Chrome.fitOrigin(winW, winH, scale)
|
|||||||
local x, y, w, h = playfieldRect(winW, winH)
|
local x, y, w, h = playfieldRect(winW, winH)
|
||||||
return x + math.floor((w - Chrome.SCREEN_W * 8 * scale) / 2),
|
return x + math.floor((w - Chrome.SCREEN_W * 8 * scale) / 2),
|
||||||
y + math.floor((h - Chrome.SCREEN_H * 8 * scale) / 2)
|
y + math.floor((h - Chrome.SCREEN_H * 8 * scale) / 2)
|
||||||
|
- Chrome.positionLift(winW, winH, scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Chrome.positionLift(winW, winH, scale)
|
||||||
|
local ok, ScreenPosition = pcall(require, "src.core.ScreenPosition")
|
||||||
|
if not ok or ScreenPosition.skinActive(winW, winH) then return 0 end
|
||||||
|
local _, _, _, h = playfieldRect(winW, winH)
|
||||||
|
return ScreenPosition.lift(h, Chrome.SCREEN_H * 8 * (scale
|
||||||
|
or Chrome.fitScale(winW, winH)), ScreenPosition.safeTop())
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A bordered box, tile coords. Leaves the draw color black for text.
|
-- A bordered box, tile coords. Leaves the draw color black for text.
|
||||||
|
|||||||
@@ -196,6 +196,15 @@ local ROWS = {
|
|||||||
return VideoMode.normalize(options.videoMode) == "borderless"
|
return VideoMode.normalize(options.videoMode) == "borderless"
|
||||||
and "FULL" or "WINDOWED"
|
and "FULL" or "WINDOWED"
|
||||||
end },
|
end },
|
||||||
|
{ label = "SCREEN POS", key = "screenPos", port = true,
|
||||||
|
cycle = function(options, delta)
|
||||||
|
local ScreenPosition = require("src.core.ScreenPosition")
|
||||||
|
options.screenPos = ScreenPosition.cycle(options.screenPos, delta)
|
||||||
|
ScreenPosition.setMode(options.screenPos)
|
||||||
|
end,
|
||||||
|
text = function(options)
|
||||||
|
return require("src.core.ScreenPosition").label(options.screenPos)
|
||||||
|
end },
|
||||||
{ id = "touchControls", label = "TOUCH PAD", port = true,
|
{ id = "touchControls", label = "TOUCH PAD", port = true,
|
||||||
text = function(options)
|
text = function(options)
|
||||||
local tc = options.touchControls
|
local tc = options.touchControls
|
||||||
|
|||||||
@@ -10068,6 +10068,15 @@ function World:draw()
|
|||||||
if self.shake then
|
if self.shake then
|
||||||
self.camera.y = self.camera.y + (self.shake.phase or 0)
|
self.camera.y = self.camera.y + (self.shake.phase or 0)
|
||||||
end
|
end
|
||||||
|
local ScreenPosition = require("src.core.ScreenPosition")
|
||||||
|
local posLift = 0
|
||||||
|
if not ScreenPosition.skinActive(w, h) then
|
||||||
|
posLift = ScreenPosition.lift(h, 144 * self:fitScale(),
|
||||||
|
ScreenPosition.safeTop())
|
||||||
|
end
|
||||||
|
if posLift > 0 then
|
||||||
|
self.camera.y = self.camera.y + posLift / s
|
||||||
|
end
|
||||||
|
|
||||||
local override = pipelineId and self:drawPipeline(pipelineId, w, h, s) or nil
|
local override = pipelineId and self:drawPipeline(pipelineId, w, h, s) or nil
|
||||||
|
|
||||||
@@ -10093,7 +10102,7 @@ function World:draw()
|
|||||||
local pad = POKEPIC.pad[math.floor(pw / 8)] or POKEPIC.pad[7]
|
local pad = POKEPIC.pad[math.floor(pw / 8)] or POKEPIC.pad[7]
|
||||||
G.push()
|
G.push()
|
||||||
G.translate(math.floor((w - 160 * sPic) / 2),
|
G.translate(math.floor((w - 160 * sPic) / 2),
|
||||||
math.floor((h - 144 * sPic) / 2))
|
math.floor((h - 144 * sPic) / 2) - posLift)
|
||||||
G.scale(sPic, sPic)
|
G.scale(sPic, sPic)
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
local function body()
|
local function body()
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check, eq = T.check, T.eq
|
||||||
|
love = love or require("tests.love_stub")
|
||||||
|
|
||||||
|
local ScreenPosition = require("src.core.ScreenPosition")
|
||||||
|
local TouchSkin = require("src.core.TouchSkin")
|
||||||
|
local Renderer = require("src.render.Renderer")
|
||||||
|
local Chrome = require("src.ui.gen2.Chrome")
|
||||||
|
local Zoom = require("src.render.Zoom")
|
||||||
|
|
||||||
|
eq(ScreenPosition.normalize(nil), "center", "no setting is CENTER")
|
||||||
|
eq(ScreenPosition.normalize("junk"), "center", "garbage degrades to CENTER")
|
||||||
|
eq(ScreenPosition.normalize("top"), "top", "top passes through")
|
||||||
|
eq(ScreenPosition.label("upper"), "UPPER", "upper reads UPPER")
|
||||||
|
|
||||||
|
local seen, v = {}, "center"
|
||||||
|
for _ = 1, 3 do
|
||||||
|
seen[#seen + 1] = ScreenPosition.label(v)
|
||||||
|
v = ScreenPosition.cycle(v, 1)
|
||||||
|
end
|
||||||
|
eq(table.concat(seen, ","), "CENTER,UPPER,TOP", "the row cycles CENTER,UPPER,TOP")
|
||||||
|
eq(ScreenPosition.cycle("top", 1), "center", "and wraps back to CENTER")
|
||||||
|
eq(ScreenPosition.cycle("center", -1), "top", "stepping back lands on TOP")
|
||||||
|
|
||||||
|
ScreenPosition.setMode("center")
|
||||||
|
eq(ScreenPosition.lift(640, 288), 0, "CENTER never lifts")
|
||||||
|
|
||||||
|
ScreenPosition.setMode("top")
|
||||||
|
eq(ScreenPosition.lift(640, 288), 176, "TOP lifts the centered origin to 0")
|
||||||
|
eq(ScreenPosition.lift(288, 288), 0, "no slack, no lift")
|
||||||
|
eq(ScreenPosition.lift(144, 288), 0, "negative slack, no lift")
|
||||||
|
eq(ScreenPosition.lift(640, 288, 40), 136, "TOP stops at the safe-area inset")
|
||||||
|
eq(ScreenPosition.lift(640, 288, 999), 0,
|
||||||
|
"a safe inset past center degrades to centered")
|
||||||
|
|
||||||
|
ScreenPosition.setMode("upper")
|
||||||
|
eq(ScreenPosition.lift(640, 288), 88, "UPPER lands halfway between")
|
||||||
|
eq(ScreenPosition.lift(640, 288, 40), 88, "a small inset leaves UPPER alone")
|
||||||
|
eq(ScreenPosition.lift(640, 288, 120), 56, "a large inset pushes UPPER down")
|
||||||
|
|
||||||
|
ScreenPosition.applyOptions({ screenPos = "top" })
|
||||||
|
eq(ScreenPosition.mode, "top", "applyOptions takes the stored key")
|
||||||
|
ScreenPosition.applyOptions(nil)
|
||||||
|
eq(ScreenPosition.mode, "center", "applyOptions without options is CENTER")
|
||||||
|
|
||||||
|
local function setWindow(w, h)
|
||||||
|
love.graphics.getDimensions = function() return w, h end
|
||||||
|
love.graphics.getPixelDimensions = function() return w, h end
|
||||||
|
end
|
||||||
|
|
||||||
|
setWindow(360, 640)
|
||||||
|
TouchSkin.setActive(nil)
|
||||||
|
Renderer:init()
|
||||||
|
Zoom.offset = 0
|
||||||
|
|
||||||
|
ScreenPosition.setMode("center")
|
||||||
|
local r = Renderer:frameRects()
|
||||||
|
eq(r.Sp, 2, "360x640 fits two whole GB pixels")
|
||||||
|
eq(r.lift, 0, "CENTER: no lift")
|
||||||
|
eq(r.oy, 176, "CENTER: the letterbox is centered")
|
||||||
|
local _, vhCenter = Renderer:worldViewSize()
|
||||||
|
|
||||||
|
ScreenPosition.setMode("top")
|
||||||
|
r = Renderer:frameRects()
|
||||||
|
eq(r.lift, 176, "TOP: the full centered slack lifts away")
|
||||||
|
eq(r.oy, 0, "TOP: the letterbox sits at the top edge")
|
||||||
|
eq(r.uoy, 0, "TOP: the UI letterbox follows")
|
||||||
|
eq(r.ox, math.floor((360 - 320) / 2), "TOP: horizontal centering is untouched")
|
||||||
|
local _, vhTop = Renderer:worldViewSize()
|
||||||
|
eq(vhTop, vhCenter + 2 * math.ceil(176 / 2),
|
||||||
|
"TOP: the world canvas grows to keep the bottom covered")
|
||||||
|
|
||||||
|
ScreenPosition.setMode("upper")
|
||||||
|
r = Renderer:frameRects()
|
||||||
|
eq(r.oy, 88, "UPPER: the letterbox centers in the upper half")
|
||||||
|
|
||||||
|
ScreenPosition.setMode("top")
|
||||||
|
local ox, oy = Chrome.fitOrigin(360, 640)
|
||||||
|
eq(oy, 0, "TOP: Gold's letterbox sits at the top edge")
|
||||||
|
eq(ox, math.floor((360 - 320) / 2), "TOP: Gold's horizontal centering is untouched")
|
||||||
|
ScreenPosition.setMode("center")
|
||||||
|
local _, cy = Chrome.fitOrigin(360, 640)
|
||||||
|
eq(cy, 176, "CENTER: Gold's letterbox is centered")
|
||||||
|
|
||||||
|
ScreenPosition.setMode("top")
|
||||||
|
local skin = assert(TouchSkin.parse([[
|
||||||
|
overlays = 1
|
||||||
|
overlay0_name = "bezel"
|
||||||
|
overlay0_full_screen = true
|
||||||
|
overlay0_normalized = true
|
||||||
|
overlay0_viewport = "0.1,0.1,0.8,0.4"
|
||||||
|
overlay0_descs = 1
|
||||||
|
overlay0_desc0 = "nul,0.5,0.5,rect,0.02,0.02"
|
||||||
|
]]))
|
||||||
|
TouchSkin.setActive(skin)
|
||||||
|
TouchSkin.setOverlayLive(false)
|
||||||
|
r = Renderer:frameRects()
|
||||||
|
eq(r.cut, true, "the skin viewport cuts the playfield")
|
||||||
|
eq(r.lift, 0, "a skin viewport disables the lift")
|
||||||
|
eq(select(1, ScreenPosition.skinActive(360, 640)), true,
|
||||||
|
"skinActive sees the viewport")
|
||||||
|
local _, sy = Chrome.fitOrigin(360, 640)
|
||||||
|
local _, cy2 = (function()
|
||||||
|
ScreenPosition.setMode("center")
|
||||||
|
return Chrome.fitOrigin(360, 640)
|
||||||
|
end)()
|
||||||
|
eq(sy, cy2, "with a skin the Gold origin ignores the mode")
|
||||||
|
TouchSkin.setActive(nil)
|
||||||
|
ScreenPosition.setMode("center")
|
||||||
|
|
||||||
|
T.finish("screen_position")
|
||||||
Reference in New Issue
Block a user