Merge pull request #705 from bryanthaboi/dev

This commit is contained in:
bryanthaboi
2026-08-02 19:11:23 -04:00
committed by GitHub
19 changed files with 371 additions and 30 deletions
+3 -2
View File
@@ -112,6 +112,7 @@ supported out of the box.
| Key | What it does |
| --------- | ---------------------------------------------------- |
| `-` / `=` | Zoom out / in (overworld; also mouse wheel) |
| `1` | Cycle GAME SPEED up (controller: R2 faster, L2 slower) |
| `2` | Cycle COLORS |
| `3` | Cycle TILT (free-roam overworld) |
| `4` | Cycle ZOOM through every level (free-roam overworld) |
@@ -121,8 +122,8 @@ supported out of the box.
| `F10` | Open / close the mod manager |
COLORS, TILT, ZOOM, GBC FX, and VOID FILL are also in the Options menu
and persist in `options.lua`.
COLORS, TILT, ZOOM, GBC FX, GAME SPEED, and VOID FILL are also in the
Options menu and persist in `options.lua`.
### Low-end devices
+6 -1
View File
@@ -325,9 +325,14 @@ return {
table.insert(rows, { "jump_if_false", base + 6 })
table.insert(rows, { "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
-- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then
-- walk out past the player. The fanfare was dropped here (#683) -- the
-- parcel scene above already plays Music_MeetRival on both arrival and
-- departure (lines 144-146), and this exit should match (#596).
table.insert(rows, { "stop_music" })
table.insert(rows, { "play_music", "Music_MeetRival" })
table.insert(rows, { "move_npc_to", 1, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
-- restore the lab theme once he's walked out, same as the Yellow port
table.insert(rows, { "play_music", "Music_OaksLab" })
ow.runner:run(rows, { npc = rival })
return true
+4 -2
View File
@@ -287,10 +287,12 @@ return {
table.insert(rows, { "label", "lost_lab" })
table.insert(rows, { "set_field", "rivalStarter", 3 })
table.insert(rows, { "label", "exit" })
-- OaksLabRivalStartsExitScript: parting shot, walk out past the
-- player, restore the lab theme
-- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then
-- walk out past the player (#683).
table.insert(rows, { "wait", 20 })
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
table.insert(rows, { "stop_music" })
table.insert(rows, { "play_music", "Music_MeetRival" })
table.insert(rows, { "move_npc_to", RIVAL, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
table.insert(rows, { "play_music", "Music_OaksLab" })
+1
View File
@@ -56,6 +56,7 @@ for mapId, holes in pairs(HOLE_FALLS) do
M[mapId].onStep = function(game, ow, x, y)
for _, h in ipairs(holes) do
if x == h[1] and y == h[2] then
require("src.core.Sound").play(game.data, "Faint_Fall")
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
return true
end
+1
View File
@@ -942,6 +942,7 @@ M.VICTORY_ROAD_3F = {
-- fall is onStep, not a collision block.
onStep = function(game, ow, x, y)
if x == 23 and y == 15 then
require("src.core.Sound").play(game.data, "Faint_Fall")
ow:startWarpTo("VICTORY_ROAD_2F", 22, 16, ow.player.facing)
return true
end
+1
View File
@@ -118,6 +118,7 @@ local MANSION_HOLES = {
M.POKEMON_MANSION_3F.onStep = function(game, ow, x, y)
for _, h in ipairs(MANSION_HOLES) do
if x == h[1] and y == h[2] then
require("src.core.Sound").play(game.data, "Faint_Fall")
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
return true
end
+2 -2
View File
@@ -1,13 +1,13 @@
{
"name": "gen1recomp App Repo",
"identifier": "com.theboisclub.gen1recomp.repo",
"iconURL": "https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/assets/logo/logo.png",
"iconURL": "https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/assets/logo/gen1recomp_cover.png",
"apps": [
{
"name": "gen1recomp",
"bundleIdentifier": "com.theboisclub.gen1recomp",
"developerName": "bryanthaboi",
"iconURL": "https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/assets/logo/logo.png",
"iconURL": "https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/assets/logo/gen1recomp_cover.png",
"localizedDescription": "Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke",
"tintColor": "3b5ca8",
"category": "games",
+97 -6
View File
@@ -8,8 +8,21 @@
-- letterbox entirely, so the surface is the Game Boy screen and nothing else.
--
-- Persisted as save.options.faithfulRes (0 = OFF). Applied from OptionsMenu
-- and on boot via Game:applyOptions. No-ops on mobile and in headless stubs
-- that lack love.window.
-- and on boot via Game:applyOptions. No-ops in headless stubs that lack
-- love.window.
--
-- MOBILE takes the other route to the same place. There is no window to
-- resize -- the window IS the screen, and it rotates -- so the lock caps the
-- RENDER scale instead: the renderer draws the Game Boy screen at exactly N
-- physical pixels per GB pixel and centres it, and the rest of the display
-- stays black. Same promise as the desktop lock (a GB pixel is exactly N
-- screen pixels, no more) reached by moving the picture rather than the
-- window. This used to return false on the first line, so the row sat in
-- OPTIONS on Android and iOS doing nothing at all.
--
-- Scale, not size, is also what makes rotation free: Renderer:fitScale runs
-- every frame off the live drawable size, so portrait and landscape both get
-- the same locked scale with the bars falling wherever the screen is longer.
local FaithfulRes = {}
@@ -17,6 +30,10 @@ FaithfulRes.WIDTH, FaithfulRes.HEIGHT = 160, 144
FaithfulRes.LEVELS = { 0, 1, 2, 3, 4 }
FaithfulRes.DEFAULT = 0
-- mobile only: the locked scale in physical pixels per GB pixel, 0 for OFF.
-- Renderer:fitScale reads it through FaithfulRes.scaleCap.
FaithfulRes.mobileScale = 0
-- conf.lua's floor for the resizable desktop window, restored when the lock
-- is released. 1X and 2X are BELOW it, so the lock has to lower the minimum
-- as well as set the size or LOVE clamps the window back up.
@@ -25,21 +42,52 @@ FaithfulRes.MIN_W, FaithfulRes.MIN_H = 480, 360
-- whether this module currently owns the window size
FaithfulRes.locked = false
-- The highest level this display can actually show.
--
-- On desktop it is 4: the levels are window sizes, and 4X is the ceiling the
-- feature shipped with. On mobile there is no window to size, so a fixed
-- 1..4 ladder is meaningless -- 4X is a quarter of a 1080p phone, and the
-- levels the panel could really use are not on the list at all. Derive it
-- from the screen instead, so a 1080x2400 phone offers up to 6X and the top
-- of the ladder is the biggest exact-pixel picture it can draw.
--
-- OFF (0) is untouched by any of this and keeps doing exactly what it always
-- did: the renderer fits and letterboxes as usual.
function FaithfulRes.maxLevel()
-- Mobile is ON or OFF. A ladder of absolute multiples is a desktop idea --
-- there it names a window size you can see. On a phone the same number
-- means a different fraction of every device, and every level below the top
-- is just a smaller picture for no reason. ON means one thing instead:
-- lock the viewport to the Game Boy's 10:9 and size it to this screen.
if FaithfulRes.isMobile() then return 1 end
return 4
end
-- the selectable ladder for this display: OFF, then 1X..maxLevel
function FaithfulRes.levels()
local out = { 0 }
for i = 1, FaithfulRes.maxLevel() do out[#out + 1] = i end
return out
end
function FaithfulRes.normalize(v)
v = math.floor(tonumber(v) or FaithfulRes.DEFAULT)
if v < 0 then return 0 end
if v > 4 then return 4 end
local max = FaithfulRes.maxLevel()
if v > max then return max end
return v
end
function FaithfulRes.label(v)
v = FaithfulRes.normalize(v)
if v == 0 then return "OFF" end
-- mobile has one ON: the level is chosen from the display, not the player
if FaithfulRes.isMobile() then return "ON" end
return tostring(v) .. "X"
end
function FaithfulRes.cycle(v, dir)
local levels = FaithfulRes.LEVELS
local levels = FaithfulRes.levels()
local cur = 1
for i, level in ipairs(levels) do
if level == FaithfulRes.normalize(v) then cur = i break end
@@ -48,6 +96,12 @@ function FaithfulRes.cycle(v, dir)
end
function FaithfulRes.isMobile()
-- POKEPORT_FORCE_MOBILE=1: take the mobile branch on a desktop build, so the
-- scale lock can be seen and driven without a device. The window is still
-- resizable, which is the point -- drag it to a phone aspect, rotate it by
-- dragging the other way, and the lock has to hold through both. Only this
-- module reads isMobile, so the override cannot leak into anything else.
if os.getenv("POKEPORT_FORCE_MOBILE") == "1" then return true end
if not love or not love.system or not love.system.getOS then return false end
local osName = love.system.getOS()
return osName == "Android" or osName == "iOS"
@@ -86,13 +140,50 @@ end
-- Push the lock into the live window. Returns true when the window is
-- locked afterwards.
-- The largest WHOLE multiple of the Game Boy screen this display can hold.
-- Integer, never fractional: a GB pixel has to be the same number of screen
-- pixels in both axes or it is not pixel perfect, it is resampled.
--
-- The leftover is black bars, and on a tall phone there is a lot of it
-- vertically -- that is simply what a 10:9 screen looks like on a 9:20
-- display, and it is what an emulator shows too.
function FaithfulRes.deviceScale()
local g = love and love.graphics
if not (g and g.getPixelDimensions) then return 1 end
local pw, ph = g.getPixelDimensions()
if not pw or not ph or pw <= 0 or ph <= 0 then return 1 end
return math.max(1, math.floor(math.min(pw / FaithfulRes.WIDTH,
ph / FaithfulRes.HEIGHT)))
end
-- The scale the renderer must lock to, or nil for "fit the window as usual".
-- Only ever set on mobile: on desktop the window itself is the lock, so
-- fitScale already lands on N and this would be a second, redundant one.
--
-- Always the device maximum. Anything less is a smaller picture for no gain,
-- which is how the first cut ended up showing a postage stamp on a 1080p
-- phone.
function FaithfulRes.scaleCap()
if not FaithfulRes.locked then return nil end
if not FaithfulRes.isMobile() then return nil end
return FaithfulRes.deviceScale()
end
function FaithfulRes.apply(v)
if FaithfulRes.isMobile() then return false end
v = FaithfulRes.normalize(v)
-- Mobile: lock the render scale instead of the window. The scale itself
-- comes from the display (deviceScale), not from v -- v only says whether
-- the lock is on. Nothing to restore on release: the renderer simply goes
-- back to filling the display.
if FaithfulRes.isMobile() then
FaithfulRes.locked = v > 0
FaithfulRes.mobileScale = FaithfulRes.locked and FaithfulRes.deviceScale() or 0
return FaithfulRes.locked
end
if not love or not love.window or not love.window.setMode
or not love.window.getMode then
return false
end
v = FaithfulRes.normalize(v)
local curW, curH, flags = love.window.getMode()
flags = flags or {}
+32
View File
@@ -509,6 +509,24 @@ function Game:wheelmoved(_, dy)
end
end
function Game:_cycleSpeed(dir)
if not (self.save and self.save.options) then return end
local busy
local ow = self.overworld
if ow then
local top = self.stack:top()
busy = ow.transitioning
or (top == ow and (
(ow.runner and ow.runner.isRunning and ow.runner:isRunning())
or (ow.scriptMoves and #ow.scriptMoves > 0)
or ow.engaging or ow.emote))
end
if busy then return end
local GameSpeed = require("src.core.GameSpeed")
self.save.options.speed = GameSpeed.cycle(self.save.options.speed, dir)
self:writeOptions()
end
function Game:keypressed(key)
if self.stack and self.stack:top() and self.stack:top().onKeyPressed then
self.stack:top():onKeyPressed(key)
@@ -546,6 +564,11 @@ function Game:keypressed(key)
elseif key == "=" then
self:zoomStep(1)
return
elseif key == "1" then
-- cycle GAME SPEED (0.25X → 200X, logic only; audio unaffected);
-- R2/L2 on gamepad do the same (see gamepadpressed)
self:_cycleSpeed(1)
return
elseif key == "2" then
-- cycle COLORS (GBC / OG / OG INV / GBC INV / CLASSIC); the pack change
-- forces Game.overworld:reloadMap, which rebuilds the live NPC array, so
@@ -626,6 +649,15 @@ function Game:gamepadpressed(joystick, button)
-- a controller is being used: the touch overlay steps aside until the
-- next screen touch (mobile only; a no-op elsewhere)
TouchControls:noteGamepad()
-- shoulder buttons cycle GAME SPEED (R2/rightshoulder = faster,
-- L2/leftshoulder = slower; same as keyboard hotkey 1)
if button == "rightshoulder" then
self:_cycleSpeed(1)
return
elseif button == "leftshoulder" then
self:_cycleSpeed(-1)
return
end
-- BindingsMenu's pad capture rides the same top-state routing as keys
local top = self.stack and self.stack:top()
if top and top.onGamepadPressed then
+1 -1
View File
@@ -18,7 +18,7 @@ local GameSpeed = {}
-- attempt is long enough that the iteration loop, not the engine, is the
-- bottleneck. Vsync caps how much a real frame can do, so past 10X the
-- multiplier is increasingly a ceiling rather than a rate.
GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75, 100,200 }
GameSpeed.LEVELS = { 1, 2, 3, 4, 10, 20, 30, 50, 75, 100, 200 }
GameSpeed.DEFAULT = 1
function GameSpeed.levelLabel(v)
+5
View File
@@ -324,6 +324,11 @@ function ItemEffects.use(data, save, itemId, target, battle, moveIndex, ow)
require("src.core.Sound").play(data, "Heal_HP")
-- a revive takes the same .healHP -> .doneHealing route, animating up
-- from the fainted mon's 0 HP (#252)
-- re-add to participants so the revived mon gets its share of exp
-- at battle end (onFaint clears the flag; revive must restore it)
if battle and battle.participants then
battle.participants[target] = true
end
return "consumed", { Strings("%s\nis revitalized!", monName(data, target)) },
{ healedFrom = 0 }
end
+17 -7
View File
@@ -559,16 +559,26 @@ local TILESET_GROUP_EXCEPTIONS = {
}
-- pokered-gbc's lobby.bst repoints the Celadon LOBBY table's flat top
-- (block 29, cells 5/6/9/10) at a duplicate tile ($5a, BROWN) so the
-- tabletop and the checkerboard floor -- both raw tile $37 -- can take
-- different palettes; the vanilla-derived blockset shares the one tile
-- id, so the RED++ atlas path re-creates the duplicate: the alias slot
-- is baked as a copy of `tile` in `group`'s colors, and the listed
-- 0-based block cells draw the alias instead of the shared tile.
-- Same block appears on CELADON_MART_ROOF (#52) and CELADON_DINER (#84).
-- at a duplicate tile ($5a, BROWN) so the tabletop and the checkerboard
-- floor -- both raw tile $37 -- can take different palettes; the
-- vanilla-derived blockset shares the one tile id, so the RED++ atlas
-- path re-creates the duplicate: the alias slot is baked as a copy of
-- `tile` in `group`'s colors, and the listed 0-based block cells draw
-- the alias instead of the shared tile.
--
-- Three LOBBY blocks share tile $37 on their flat surfaces:
-- block 29: 2x2 table top at cells 5/6/9/10
-- block 45: 2-tile strip at cells 13/14
-- block 49: 2-tile strip at cells 1/2
-- CELADON_DINER uses all three (#84, #85, #86); CELADON_MART_ROOF
-- uses only block 29 (#52/#53).
local LOBBY_TABLE_TOP_ALIAS = {
{ block = 29, cells = { [5] = true, [6] = true, [9] = true, [10] = true },
tile = 0x37, alias = 0x5a, group = 5 },
{ block = 45, cells = { [13] = true, [14] = true },
tile = 0x37, alias = 0x5a, group = 5 },
{ block = 49, cells = { [1] = true, [2] = true },
tile = 0x37, alias = 0x5a, group = 5 },
}
PaletteFX.TILE_ALIASES = {
CELADON_MART_ROOF = LOBBY_TABLE_TOP_ALIAS,
+27 -1
View File
@@ -12,6 +12,8 @@ local PaletteFX = require("src.render.PaletteFX")
local Pipelines = require("src.render.Pipelines")
local PixelCanvas = require("src.render.PixelCanvas")
local Runtime = require("src.mods.Runtime")
-- leaf module (no renderer dependency), so requiring it here cannot cycle
local FaithfulRes = require("src.core.FaithfulRes")
local Renderer = {}
@@ -124,7 +126,16 @@ end
function Renderer:fitScale()
local _, _, pw, ph = displayMetrics()
local w, h = self:uiSize()
return math.max(1, math.floor(math.min(pw / w, ph / h)))
local s = math.max(1, math.floor(math.min(pw / w, ph / h)))
-- FAITHFUL RATIO on mobile locks the scale here rather than by resizing the
-- window, which a phone does not have (see src/core/FaithfulRes.lua). The
-- cap is the largest WHOLE multiple the display holds, so the picture is as
-- big as exact pixels allow and the remainder is bars. Computed per frame
-- off the live drawable size, so a rotate re-derives it with nothing to
-- re-apply.
local cap = FaithfulRes.scaleCap()
if cap and cap < s then s = cap end
return s
end
-- Integer framebuffer pixels per GB pixel for the UI pass.
@@ -224,6 +235,21 @@ end
-- tilt is inactive).
function Renderer:worldViewSize()
local _, _, pw, ph = 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
-- the UI blit while the map kept filling the screen -- and showed MORE of
-- the map, because a smaller scale fits more world pixels in.
--
-- Size the view against the LOCKED VIEWPORT rather than the display. A
-- desktop lock gets this for free by making the window exactly 160N x 144N;
-- 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 cap then
local uiw, uih = self:uiSize()
pw, ph = uiw * cap, uih * cap
end
local sp = Zoom.scale(self:fitScale())
local vw, vh = math.ceil(pw / sp), math.ceil(ph / sp)
-- Even sizes keep Camera:follow on integer pixels (viewW/2 is integral),
+1 -1
View File
@@ -354,7 +354,7 @@ local function buildRows(game)
-- Game Boy screen with no letterbox at all. Sits next to VIDEO MODE
-- because it overrides it: holding an exact size means dropping
-- fullscreen.
{ id = "faithfulRes", label = Strings("FAITHFUL RES"),
{ id = "faithfulRes", label = Strings("FAITHFUL RATIO"),
value = function(g)
return FaithfulRes.label(g.save.options.faithfulRes)
end,
+2 -1
View File
@@ -203,7 +203,8 @@ function SummaryMenu:draw()
local mdef = data.moves[mv.id]
Font.draw(mdef.name, 16, y)
Font.draw(Strings("PP"), 88, y + 8)
Font.draw(("%2d/%2d"):format(mv.pp, mdef.pp), 112, y + 8)
local maxPP = mdef.pp + (mv.ppUps or 0) * math.floor(mdef.pp / 5)
Font.draw(("%2d/%2d"):format(mv.pp, maxPP), 112, y + 8)
else
Font.draw("-", 16, y)
Font.draw("--", 112, y + 8)
+10 -1
View File
@@ -2580,8 +2580,13 @@ function OverworldState:openPC(onDone)
-- (engine/menus/pokemon_pc.asm gates on EVENT_MET_BILL; we reach that
-- when Bill hands over the SS Ticket)
local metBill = flags.EVENT_MET_BILL or flags.EVENT_GOT_SS_TICKET
-- keepOpen so B in the sub-PC returns here instead of exiting the
-- PC session (#695); the sub-PC screens (BoxMenu, PlayerPC) already
-- use keepOpen for their own rows, matching the original ROM's flow
-- where the main menu stays underneath.
table.insert(items, {
label = metBill and "BILL'S PC" or Strings("SOMEONE'S PC"),
keepOpen = true,
onSelect = function()
require("src.core.Sound").play(Game.data, "Enter_PC")
Screens.push(Game, "BoxMenu")
@@ -2592,6 +2597,7 @@ function OverworldState:openPC(onDone)
-- the player's item storage is always available
table.insert(items, {
label = (Game.save.player.name or "RED") .. "'s PC",
keepOpen = true,
onSelect = function()
Screens.push(Game, "PlayerPC")
done()
@@ -2602,6 +2608,7 @@ function OverworldState:openPC(onDone)
if flags.EVENT_GOT_POKEDEX then
table.insert(items, {
label = Strings("PROF.OAK's PC"),
keepOpen = true,
onSelect = function()
self:openOaksPC(done)
end,
@@ -3774,7 +3781,9 @@ function OverworldState:takeWarp(warpDef)
self:startWarpTo(destMap, x, y, facing)
return
elseif pad == "hole" then
-- falling through a hole: no door SFX, no walk-out step
-- falling through a hole: Faint_Fall plays while the player drops,
-- matching the boulder-hole Faint_Thud at line 3618 (#694)
require("src.core.Sound").play(Game.data, "Faint_Fall")
self:startWarpTo(destMap, x, y, facing)
return
end
+15 -4
View File
@@ -1,4 +1,4 @@
-- FAITHFUL RES: lock the window to an exact 160x144 multiple so the surface
-- FAITHFUL RATIO: lock the window to an exact 160x144 multiple so the surface
-- is the Game Boy screen with no letterbox at all.
--
-- The interesting parts are the two things a naive setMode gets wrong: the
@@ -121,11 +121,22 @@ T.eq(calls[1].flags.minwidth, FaithfulRes.MIN_W, "with conf.lua's floor restored
T.eq(calls[1].flags.minheight, FaithfulRes.MIN_H, "on both axes")
T.eq(FaithfulRes.locked, false, "and the module no longer claims the window")
-- mobile has no resizable window to lock
-- Mobile has no resizable window to lock, so it locks the RENDER scale
-- instead and never calls setMode. It used to report unlocked and do
-- nothing at all, which is why the OPTIONS row was inert on Android and iOS;
-- tests/engine/faithful_res_mobile.lua covers the scale side.
calls = stubWindow(1)
love.system = { getOS = function() return "Android" end }
T.eq(FaithfulRes.apply(4), false, "mobile reports unlocked")
T.eq(#calls, 0, "and never touches the window")
T.eq(FaithfulRes.apply(4), true, "mobile locks, by capping the render scale")
-- the level asked for is irrelevant on mobile: ON is ON, and the scale is
-- read off the display so the picture is as big as exact pixels allow
T.eq(FaithfulRes.scaleCap(), FaithfulRes.deviceScale(),
"and the scale comes from the display, not from the level")
T.eq(#calls, 0, "still without ever touching the window")
T.eq(FaithfulRes.apply(0), false, "OFF releases it")
T.eq(FaithfulRes.scaleCap(), nil, "and the cap goes away with it")
T.eq(#calls, 0, "the window is left alone either way")
FaithfulRes.mobileScale = 0
love.window, love.system = savedWindow, savedSystem
if love.graphics then
+134
View File
@@ -0,0 +1,134 @@
-- FAITHFUL RATIO on Android / iOS.
--
-- On mobile the setting is ON or OFF, and ON means one thing: lock the
-- viewport to the Game Boy's 10:9 at the largest WHOLE multiple this screen
-- can hold, centred, black around it -- the way an emulator opens a Game Boy
-- game on a phone.
--
-- Three things had to be true and none of them were:
--
-- * it had to apply at all. FaithfulRes.apply returned false on its first
-- line for mobile, so the OPTIONS row did nothing on Android and iOS.
-- A phone has no window to resize, so the lock caps the RENDER scale.
--
-- * it had to be sized for the device. The first cut kept the desktop's
-- absolute 1X-4X ladder, which names a window size you can see on a
-- desktop and means a different fraction of every phone: 4X was a quarter
-- of a 1080p display and 5X/6X were not on the list at all. The scale is
-- read off the display now, not chosen by the player.
--
-- * it had to work in the OVERWORLD. The world pass deliberately expands
-- to cover the whole display so letterbox voids become more map. So the
-- lock shrank the UI blit while the map kept filling the screen -- and
-- showed MORE map, since a smaller scale fits more world pixels in.
--
-- Pixel perfect throughout: whole multiples only. The leftover is bars, and
-- on a 9:20 phone there is a lot of it vertically. That is what a 10:9
-- screen looks like on a tall display; stretching to reach the edges would
-- resample every pixel, which is the one thing this setting exists to refuse.
-- luajit tests/engine/faithful_res_mobile.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local FaithfulRes = require("src.core.FaithfulRes")
local Renderer = require("src.render.Renderer")
local Zoom = require("src.render.Zoom")
local g = love.graphics
local realDims, realPixelDims = g.getDimensions, g.getPixelDimensions
local realOS = love.system and love.system.getOS
local savedOffset = Zoom.offset
love.system = love.system or {}
local function pose(w, h, osName)
love.system.getOS = function() return osName or "Android" end
g.getDimensions = function() return w, h end
g.getPixelDimensions = function() return w, h end
end
Renderer.uiWidth, Renderer.uiHeight = Renderer.WIDTH, Renderer.HEIGHT
Zoom.offset = 0
-- ------------------------------------------------------- it applies at all
pose(1080, 2400) -- a Pixel 7, portrait
T.eq(FaithfulRes.isMobile(), true, "the fixture reads as a phone")
T.eq(FaithfulRes.apply(1), true, "FAITHFUL RATIO applies on mobile at all now")
T.eq(FaithfulRes.locked, true, "and reports itself locked")
-- ------------------------------------------------- one ON, sized by the device
T.eq(FaithfulRes.maxLevel(), 1, "mobile offers ON, not a ladder of multiples")
T.eq(#FaithfulRes.levels(), 2, "so the row is exactly OFF and ON")
T.eq(FaithfulRes.label(1), "ON", "and ON is spelled ON, not 1X")
T.eq(FaithfulRes.label(0), "OFF", "with OFF unchanged")
-- 1080/160 = 6.75 and 2400/144 = 16.6, so the largest WHOLE multiple is 6
T.eq(FaithfulRes.deviceScale(), 6, "the scale comes off the display: 6x here")
T.eq(FaithfulRes.scaleCap(), 6, "and that is what the renderer is told to lock")
T.eq(Renderer:fitScale(), 6, "so a GB pixel is exactly 6 screen pixels")
-- the player never picks a smaller one, which is how the first cut managed to
-- draw a postage stamp on a 1080p phone
T.eq(FaithfulRes.normalize(3), 1, "any ON value is just ON")
FaithfulRes.apply(1)
T.eq(Renderer:fitScale(), 6, "and always lands on the device maximum")
-- --------------------------------------------------------- the overworld
FaithfulRes.apply(0)
local unlockedW = Renderer:worldViewSize()
T.check(unlockedW > 160, "unlocked, the overworld view covers the whole display")
FaithfulRes.apply(1)
local lockedW, lockedH = Renderer:worldViewSize()
T.eq(lockedW, 160, "locked, the overworld shows exactly a GB screen wide")
T.eq(lockedH, 144, "and exactly a GB screen tall")
T.check(lockedW < unlockedW,
"so the lock SHRINKS the map area instead of growing it")
-- ------------------------------------------------------ pixel perfect
-- 6 x 160 = 960 of 1080 wide. Reaching the edges would need 6.75, which
-- resamples every pixel; the bars are the honest answer.
local cap = FaithfulRes.scaleCap()
T.eq(cap, math.floor(cap), "the locked scale is a whole number, never fractional")
T.eq(160 * cap, 960, "which puts the GB screen at 960 of 1080 pixels wide")
-- ------------------------------------------------------- rotation is free
-- fitScale reads the live drawable size every frame, so a rotate re-derives
-- the scale with nothing to re-apply
pose(1080, 2400); FaithfulRes.apply(1)
T.eq(Renderer:fitScale(), 6, "portrait locks at 6x")
pose(2400, 1080); FaithfulRes.apply(1)
T.eq(Renderer:fitScale(), 7, "landscape re-derives to 7x (1080/144), still whole")
T.eq(Renderer:worldViewSize(), 160, "and the overworld stays locked through it")
-- ---------------------------------------------------------- OFF is OFF
-- Nothing about OFF changed: it is the behaviour the game already had.
pose(1080, 2400)
FaithfulRes.apply(0)
T.eq(FaithfulRes.locked, false, "OFF releases the lock")
T.eq(FaithfulRes.scaleCap(), nil, "with no cap on the renderer")
T.eq(Renderer:fitScale(), 6, "the UI fits exactly as it did before")
T.check(Renderer:worldViewSize() > 160, "and the overworld fills the screen again")
-- ------------------------------------------------- desktop is untouched
pose(1280, 800, "Windows")
T.eq(FaithfulRes.isMobile(), false, "the fixture reads as desktop")
T.eq(FaithfulRes.maxLevel(), 4, "desktop keeps its 1X-4X ladder")
T.eq(FaithfulRes.label(2), "2X", "and its labels")
T.eq(FaithfulRes.scaleCap(), nil, "no cap: the window itself is the lock there")
T.eq(Renderer:fitScale(), 5, "so fitScale is untouched (800/144 = 5)")
g.getDimensions, g.getPixelDimensions = realDims, realPixelDims
if realOS then love.system.getOS = realOS end
Zoom.offset = savedOffset
FaithfulRes.locked = false
FaithfulRes.mobileScale = 0
T.finish("faithful ratio mobile")
+12 -1
View File
@@ -621,7 +621,9 @@ check(PaletteFX.pal({ palettes = nil }, "ROUTE") == gbc.palettes.ROUTE,
check(PaletteFX.effectiveColors(gbc.palettes.MEWMON) == gbc.palettes.MEWMON,
"RED++ passes zone colors through like GBC")
-- issue #84: CELADON_DINER shares LOBBY block 29 (table top) with
-- CELADON_MART_ROOF (#52); both need the $37->$5a BROWN alias
-- CELADON_MART_ROOF (#52); both need the $37->$5a BROWN alias.
-- Issue #689: blocks 45 and 49 also form tables with tile $37 on their
-- flat surfaces; CELADON_DINER uses all three.
do
local aliases = PaletteFX.TILE_ALIASES
local roof = aliases and aliases.CELADON_MART_ROOF
@@ -630,11 +632,20 @@ do
"CELADON_MART_ROOF and CELADON_DINER both have TILE_ALIASES")
check(diner == roof,
"diner reuses the same lobby table-top alias as the mart roof")
check(#diner == 3, "three LOBBY table blocks have the tile alias")
local al = diner and diner[1]
check(al and al.block == 29 and al.tile == 0x37 and al.alias == 0x5a
and al.group == 5 and al.cells[5] and al.cells[6]
and al.cells[9] and al.cells[10],
"lobby table-top alias remaps block 29 cells 5/6/9/10")
al = diner and diner[2]
check(al and al.block == 45 and al.tile == 0x37 and al.alias == 0x5a
and al.group == 5 and al.cells[13] and al.cells[14],
"lobby table-top alias remaps block 45 cells 13/14")
al = diner and diner[3]
check(al and al.block == 49 and al.tile == 0x37 and al.alias == 0x5a
and al.group == 5 and al.cells[1] and al.cells[2],
"lobby table-top alias remaps block 49 cells 1/2")
end
-- issue #128: RED++'s gbc pack is Red-derived; Blue must keep ROM LOGO1
-- (and the Blue-only SLOTS* rows) so the title ribbon is blue, not red