mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #705 from bryanthaboi/dev
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user