Compare commits

..

19 Commits

Author SHA1 Message Date
bryanthaboi 1fa7c0eb43 Merge pull request #705 from bryanthaboi/dev 2026-08-02 19:11:23 -04:00
bryanthaboi 839b238e74 Merge pull request #699 from ShaneMcGovernIE/feature/3x-speed-and-shoulder-hotkeys 2026-08-02 19:08:25 -04:00
Shane McGovern ec22d57cd1 Document GAME SPEED hotkeys in README
Maintainer review on #699 asked to document the hotkeys. Added key 1
(cycle speed up) and controller R2/L2 to the README Hotkeys table, and
GAME SPEED to the Options-menu note.
2026-08-03 00:07:31 +01:00
bryanthaboi 23ef283272 Merge pull request #701 from ShaneMcGovernIE/fix/revive-exp-participants 2026-08-02 19:02:16 -04:00
bryanthaboi 327579f9b6 Merge pull request #700 from ShaneMcGovernIE/fix/pp-up-stats-display 2026-08-02 19:02:06 -04:00
Shane McGovern e5f9d2903d Fix revived Pokemon not receiving experience share
When a Pokemon faints in battle, onFaint() clears it from the battle
participant set. The revive item restored HP but never re-added the
mon to self.participants, so at awardExp() time the revived mon passed
the HP check but failed the participant gate -- getting no exp.

Fix: re-add the revived mon to battle.participants in the revive item
effect, so it's counted as a participant and receives its share of
experience at battle end.

Fixes #648
2026-08-02 23:22:34 +01:00
Shane McGovern 1f3aa4e111 Fix PP UP max PP not shown on stats/summary screen
The SummaryMenu (pause menu stats screen and in-battle stats screen)
displayed the base PP from the move definition as the max value,
ignoring PP Up bonuses. After using a PP UP, the screen would show
e.g. 6/5 instead of 6/6.

Fix: calculate maxPP with the PP Up bonus (basePP + ppUps * basePP/5),
matching the formula used everywhere else -- battle fight menus,
ETHER restore, Pokemon Center heal, link protocol, and save editor.

Fixes #641
2026-08-02 23:16:10 +01:00
Shane McGovern 3f88d1c49a Add 3X game speed and R2/L2 shoulder button speed hotkeys
Add 3X as a speed option between 2X and 4X in GameSpeed.LEVELS (#677).

Add controller hotkeys: rightshoulder (R2) cycles speed up through the
level list, leftshoulder (L2) cycles speed down. Keyboard equivalent
is hotkey 1 (cycles up). All hotkeys are gated during transitions,
scripted cutscenes, and link play (same guards as the color hotkey
at key 2).

The _cycleSpeed helper wraps the save-options update with the same
busy/overworld guard used by the existing color-cycle hotkey.

Fixes #677
2026-08-02 23:10:06 +01:00
bryanthaboi 5348ba1c65 Merge pull request #690 from spiritsnails/fix/faithful-ratio-mobile 2026-08-02 17:58:41 -04:00
bryanthaboi 2f35fd44a1 Merge pull request #693 from ShaneMcGovernIE/fix/celadon-diner-table-alias 2026-08-02 17:58:17 -04:00
bryanthaboi 470fe70f07 Merge pull request #696 from ShaneMcGovernIE/fix/oaks-lab-rival-exit-music 2026-08-02 17:57:58 -04:00
bryanthaboi 399bf557f3 Merge pull request #698 from ShaneMcGovernIE/fix/pc-back-and-hole-sfx 2026-08-02 17:57:44 -04:00
Shane McGovern fc2ca6cc26 Fix PC B-button navigation and hole-fall sound effect
Issue #695: Pressing B in PC submenus (BoxMenu, PlayerPC) was exiting
the entire PC session instead of returning to the main PC menu. The
three main-menu items (Bill's PC, player's PC, Prof. Oak's PC) were
missing keepOpen=true, so selecting one popped the main menu off the
stack. Added keepOpen to all three, matching the pattern already used
by BoxMenu and PlayerPC's own rows.

Issue #694: Falling through boulder holes in Seafoam Islands, Victory
Road, and Pokemon Mansion played no sound effect. Added Faint_Fall sfx
before every hole warp -- the scripted onStep holes in seafoam.lua,
story.lua, and story6.lua, plus the warp-tile-based hole detection in
OverworldController takeWarp. Faint_Fall is the companion to Faint_Thud
(already played when boulders fall into holes).

Fixes #695
Fixes #694
2026-08-02 22:11:15 +01:00
Shane McGovern d9d42ec956 Play rival encounter music when rival leaves Oak's Lab after battle
The parcel scene in Oak's Lab plays Music_MeetRival on both the rival's
arrival and departure (lines 144-146 in oaks_lab.lua), but the post-battle
onStep exit sequence only played the fanfare when the rival approached
(fixed in #596). It was missing when the rival walks out after the battle.

Add stop_music + play_music Music_MeetRival before the rival's exit
walk-out in both oaks_lab.lua and oaks_lab_yellow.lua, matching the
parcel scene's double-fanfare pattern from the original ROM.

Fixes #683
2026-08-02 21:49:56 +01:00
Shane McGovern 5e41c74682 Add tile aliases for LOBBY table blocks 45 and 49
The Celadon Diner uses three LOBBY table blocks that share tile 0x37 on
their flat surfaces. Only block 29 had the 0x37->0x5a BROWN alias; blocks
45 and 49 showed raw tile 0x37 in ROOF (blue-gray), creating a blue
square on the second/third tables with the Advanced Colors preset.

Add alias entries for blocks 45 (cells 13/14) and 49 (cells 1/2).

Fixes #689
2026-08-02 21:20:28 +01:00
spiritsnails 02ad846dfa fix: FAITHFUL RATIO works on Android and iOS
apply() returned false on its first line for mobile, so the option did
nothing there. A phone has no window to resize, so the lock caps the
render scale instead: the largest whole multiple of 160x144 the display
holds, centred, black around it.

Two parts beyond that. The scale is read off the display rather than from
the desktop's 1X-4X ladder, which named a different fraction of every
device and left the useful levels off the list; mobile shows ON or OFF.
And the world pass, which expands to cover the whole display so letterbox
becomes more map, is now sized against the locked viewport, so the lock
reaches the overworld instead of showing more of it.

Pixel perfect throughout, whole multiples only. Desktop and OFF are
unchanged. Renames the row to FAITHFUL RATIO on both platforms; the saved
key stays faithfulRes so existing settings carry over.
2026-08-02 12:44:38 -06:00
bryanthaboi 4e7eda65ed Merge pull request #685 from castdrian/metal 2026-08-02 14:34:08 -04:00
Adrian Castro 3fabe4f591 fix(ios): use square mobile icon in AltSource 2026-08-02 19:27:55 +02:00
github-actions 81234ef2ab chore(ios): update app-repo.json [skip ci] 2026-08-02 13:21:15 -04:00
19 changed files with 378 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
+9 -2
View File
@@ -1,17 +1,24 @@
{
"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",
"versions": [
{
"version": "0.1.59",
"date": "2026-08-02",
"size": 8334731,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.59/gen1recomp-0.1.59-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #668 No Jingle playing when receiving first Pokemon from Oak\n- #671 faint animation error\n\n## Contributors\n\n- @bryanthaboi\n- @castdrian\n- @jherediagu\n- @spiritsnails\n- Shane McGovern"
},
{
"version": "0.1.58",
"date": "2026-08-02",
+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