mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
Merge branch 'dev' into feat/switch-nx
Bring feat/switch-nx up to date with origin/dev (72 commits). Resolve Input/RomImporter conflicts by keeping GamepadMap (NX face remap + dual-path gate) while adopting upstream joyBindings rebinds (#632) and Enable-all mods (#647). Gate shoulder GAME SPEED hotkeys when Select is held so Select+L display chords still work. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -123,8 +123,10 @@ function BindingsMenu:beginCapture(item)
|
||||
self.pending = nil
|
||||
self.onKeyPressed = BindingsMenu.captureKey
|
||||
self.onGamepadPressed = BindingsMenu.capturePad
|
||||
self.onJoystickPressed = BindingsMenu.captureJoy
|
||||
self.onKeyReleased = BindingsMenu.captureKeyRelease
|
||||
self.onGamepadReleased = BindingsMenu.capturePadRelease
|
||||
self.onJoystickReleased = BindingsMenu.captureJoyRelease
|
||||
end
|
||||
|
||||
function BindingsMenu:endCapture()
|
||||
@@ -132,8 +134,10 @@ function BindingsMenu:endCapture()
|
||||
self.pending = nil
|
||||
self.onKeyPressed = nil
|
||||
self.onGamepadPressed = nil
|
||||
self.onJoystickPressed = nil
|
||||
self.onKeyReleased = nil
|
||||
self.onGamepadReleased = nil
|
||||
self.onJoystickReleased = nil
|
||||
end
|
||||
|
||||
-- Escape is the capture's way out, so it is never captured: every other
|
||||
@@ -169,6 +173,27 @@ function BindingsMenu:capturePadRelease(button)
|
||||
end
|
||||
end
|
||||
|
||||
-- A stick SDL has no game-controller-database entry for reports plain
|
||||
-- button numbers instead of names, so its capture stores "joyN" in the
|
||||
-- SAME pad slot every other controller uses (#632). The row's right
|
||||
-- column, storeBinding's swap and START's reset-all then need no special
|
||||
-- case, and src/core/Input.lua's applyBindings is the only place that has
|
||||
-- to decode the name. `raw` keeps the two release hooks apart: p.value is
|
||||
-- "joy3" here and an SDL button name there, so neither can claim the
|
||||
-- other's release. Game only routes a raw stick to these hooks, so a
|
||||
-- recognized pad still records its readable SDL name.
|
||||
function BindingsMenu:captureJoy(button)
|
||||
if self.pending then return self:endCapture() end
|
||||
self.pending = { slot = "pad", value = "joy" .. button, raw = true }
|
||||
end
|
||||
|
||||
function BindingsMenu:captureJoyRelease(button)
|
||||
local p = self.pending
|
||||
if p and p.raw and p.value == "joy" .. button then
|
||||
self:storeBinding("pad", p.value)
|
||||
end
|
||||
end
|
||||
|
||||
function BindingsMenu:storeBinding(slot, value)
|
||||
local item = self.capture
|
||||
self:endCapture()
|
||||
|
||||
+10
-10
@@ -164,18 +164,18 @@ local function release(game)
|
||||
local mon = box[list.index]
|
||||
if not mon then return end
|
||||
local name = monName(game, mon)
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
Strings("Once released,\n%s is\ngone forever. OK?", name), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
Strings("Once released,\n%s is\ngone forever. OK?", name), nil, {
|
||||
defaultNo = true, noSound = true,
|
||||
choice = function(yes)
|
||||
if not yes then return end
|
||||
table.remove(box, list.index)
|
||||
require("src.core.Sound").playCry(game.data, mon.species)
|
||||
game.stack:push(TextBox.new(game,
|
||||
Strings("%s was\nreleased outside.\fBye %s!", name, name)))
|
||||
list:removeCurrent()
|
||||
end, { defaultNo = true, noSound = true }))
|
||||
end))
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
}))
|
||||
end
|
||||
@@ -196,16 +196,16 @@ local function changeBox(game)
|
||||
onChoose = function(item, list)
|
||||
-- the original asks BEFORE switching ("When you change a #MON
|
||||
-- BOX, data will be saved. OK?"); declining aborts the change
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
Strings("When you change a\nPOKéMON BOX, data\nwill be saved. OK?"), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
Strings("When you change a\nPOKéMON BOX, data\nwill be saved. OK?"), nil, {
|
||||
noSound = true,
|
||||
choice = function(yes)
|
||||
if not yes then return end
|
||||
game.save.currentBox = item.value
|
||||
if game.writeSave then game:writeSave() end
|
||||
list:close()
|
||||
end, { noSound = true }))
|
||||
end))
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
}))
|
||||
end
|
||||
|
||||
+33
-4
@@ -3,6 +3,7 @@
|
||||
local Font = require("src.render.Font")
|
||||
local Theme = require("src.ui.Theme")
|
||||
local Strings = require("src.core.Strings")
|
||||
local Timing = require("src.core.Timing")
|
||||
|
||||
local ChoiceBox = {}
|
||||
ChoiceBox.__index = ChoiceBox
|
||||
@@ -15,6 +16,12 @@ function ChoiceBox.new(game, onChoose, opts)
|
||||
self.index = (opts and opts.defaultNo) and 2 or 1
|
||||
-- BIT_NO_MENU_BUTTON_SOUND: PC-session prompts stay silent
|
||||
self.noSound = (opts and opts.noSound) or false
|
||||
-- Only a choice box sitting on top of an ANCHORED dialogue box rides the
|
||||
-- anchor with it (TextBox passes it). A bare one -- the battle's switch
|
||||
-- offer, a shop or PC confirm -- belongs over the screen that pushed it,
|
||||
-- and docking it to the window edge instead tears it off that screen by
|
||||
-- however far the letterbox sits from the edge.
|
||||
self.anchor = opts and opts.anchor or nil
|
||||
local box = Theme.choiceBox
|
||||
self.tx = (opts and opts.tx) or box.tx
|
||||
self.ty = (opts and opts.ty) or box.ty
|
||||
@@ -25,6 +32,19 @@ end
|
||||
|
||||
function ChoiceBox:update(dt)
|
||||
local input = self.game.input
|
||||
-- Both branches of DisplayTwoOptionMenu hold 15 frames with the menu still
|
||||
-- on screen before TwoOptionMenu_RestoreScreenTiles hands control back
|
||||
-- (engine/menus/text_box.asm:322-323, :333-334).
|
||||
if self.pending ~= nil then
|
||||
self.holdFrames = self.holdFrames - 1
|
||||
if self.holdFrames <= 0 then
|
||||
local yes = self.pending
|
||||
self.pending = nil
|
||||
self.game.stack:pop()
|
||||
self.onChoose(yes)
|
||||
end
|
||||
return
|
||||
end
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.index = self.index == 1 and 2 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
@@ -32,19 +52,28 @@ function ChoiceBox:update(dt)
|
||||
if not self.noSound then
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
end
|
||||
self.game.stack:pop()
|
||||
self.onChoose(self.index == 1)
|
||||
self.pending = (self.index == 1)
|
||||
self.holdFrames = Timing.YES_NO_ANSWER
|
||||
elseif input:wasPressed("b") then
|
||||
if not self.noSound then
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
end
|
||||
self.game.stack:pop()
|
||||
self.onChoose(false)
|
||||
-- .choseSecondMenuItem writes wCurrentMenuItem = 1 before the hold, so
|
||||
-- the cursor visibly snaps to NO for those 15 frames
|
||||
self.index = 2
|
||||
self.pending = false
|
||||
self.holdFrames = Timing.YES_NO_ANSWER
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceBox:draw()
|
||||
local tx, ty, tw, th = self.tx, self.ty, self.tw, self.th
|
||||
-- rides the same bottom anchor as the dialogue box it sits above, so the
|
||||
-- pair travels together (the anchor keeps each element's gap from the edge)
|
||||
local r = self.anchor and self.game and self.game.renderer
|
||||
if r and r.setUIAnchor then
|
||||
r:setUIAnchor(tx * 8, ty * 8, tw * 8, th * 8, self.anchor)
|
||||
end
|
||||
Font.drawBox(tx, ty, tw, th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("YES"), (tx + 2) * 8, (ty + 1) * 8)
|
||||
|
||||
@@ -33,6 +33,10 @@ local IntroMovie = {}
|
||||
IntroMovie.__index = IntroMovie
|
||||
IntroMovie.isOpaque = true
|
||||
|
||||
-- Same as the title screen: full-bleed art, no world behind it, no player zoom
|
||||
-- to respect, so fill the window rather than sit at the fixed integer scale.
|
||||
function IntroMovie:wantsFillScale() return true end
|
||||
|
||||
-- SGB intro palettes: the splash uses PalPacket_GameFreakIntro (logo
|
||||
-- GAMEFREAK, falling star columns RED/VIRIDIAN/BLUEMON), the attract
|
||||
-- fight PalPacket_NidorinoIntro (PURPLEMON letterbox, BLACK bars)
|
||||
|
||||
@@ -50,6 +50,9 @@ function Menu.new(game, items, opts)
|
||||
-- only menus whose real mask includes PAD_START -- the start menu
|
||||
-- (engine/menus/draw_start_menu.asm) -- opt in here.
|
||||
self.startCloses = opts.startCloses or false
|
||||
-- screen-edge anchor for this menu (see Menu:draw); nil keeps it in the
|
||||
-- classic centred letterbox
|
||||
self.anchor = opts.anchor
|
||||
self.onCancel = opts.onCancel
|
||||
-- BIT_NO_MENU_BUTTON_SOUND (wMiscFlags): the PC session runs its
|
||||
-- menus silent (home/window.asm HandleMenuInput_)
|
||||
@@ -104,6 +107,14 @@ function Menu:update(dt)
|
||||
end
|
||||
|
||||
function Menu:draw()
|
||||
-- opts.anchor opts a menu out of the centred letterbox and onto a screen
|
||||
-- edge (the START menu asks for "topright"). Only menus that ask for it
|
||||
-- move; every other menu is placed exactly as before.
|
||||
local r = self.anchor and self.game and self.game.renderer
|
||||
if r and r.setUIAnchor then
|
||||
r:setUIAnchor(self.tx * 8, self.ty * 8,
|
||||
self.tw * 8, self.th * 8, self.anchor)
|
||||
end
|
||||
Font.drawBox(self.tx, self.ty, self.tw, self.th)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local visible = (self.maxVisible and math.min(self.maxVisible, #self.items))
|
||||
|
||||
@@ -20,6 +20,16 @@ local OakSpeech = {}
|
||||
OakSpeech.__index = OakSpeech
|
||||
OakSpeech.isOpaque = true
|
||||
|
||||
-- The speech is a white field with a pic on it, and its dialogue box docks to
|
||||
-- the WINDOW's bottom edge (Renderer:setUIAnchor, via TextBox). The white it
|
||||
-- fills below is only the 160x144 UI canvas, so once the box moved to the
|
||||
-- window edge the two stopped touching: black letterbox showed between the
|
||||
-- bottom of Oak's white and the top of the box he is speaking from. Filling
|
||||
-- the voids with the paper shade -- the same opt-in a battle uses -- puts the
|
||||
-- box back on the field. Not a literal 1,1,1: the canvas is colorized, so
|
||||
-- endFrame matches it with PaletteFX.paperShade.
|
||||
OakSpeech.letterboxWhite = true
|
||||
|
||||
-- FadeInIntroPic runs a 6-step palette fade; MovePicLeft wipes the mon
|
||||
-- sprite in from the right. Both play out before the beat's text prints.
|
||||
local FADE_FRAMES = 24
|
||||
@@ -46,6 +56,8 @@ local FALLBACKS = {
|
||||
_OakSpeechText3 = Strings.source("{PLAYER}!\fYour very own\nPOKéMON legend is\vabout to unfold!\fA world of dreams\nand adventures\vwith POKéMON\vawaits! Let's go!"),
|
||||
_IntroducePlayerText = Strings.source("First, what is\nyour name?"),
|
||||
_IntroduceRivalText = Strings.source("This is my grand-\nson. He's been\vyour rival since\vyou were a baby.\f...Erm, what is\nhis name again?"),
|
||||
_YourNameIsText = Strings.source("Right! So your\nname is {PLAYER}!"),
|
||||
_HisNameIsText = Strings.source("That's right! I\nremember now! His\vname is {RIVAL}!"),
|
||||
}
|
||||
|
||||
local function textOr(game, key)
|
||||
@@ -152,6 +164,14 @@ function OakSpeech.defaultSteps(speech)
|
||||
presetsWho = "player",
|
||||
presetsFallback = { "RED", "ASH", "JACK" },
|
||||
},
|
||||
{
|
||||
-- oak_speech.asm prints YourNameIsText right after the naming screen
|
||||
-- returns ("Right! So your name is RED!"); the port went straight on
|
||||
-- to the rival and dropped it, in every language.
|
||||
id = "confirm_player_name",
|
||||
kind = "say",
|
||||
textKey = "_YourNameIsText",
|
||||
},
|
||||
{
|
||||
id = "ask_rival_name",
|
||||
kind = "say",
|
||||
@@ -166,6 +186,12 @@ function OakSpeech.defaultSteps(speech)
|
||||
presetsWho = "rival",
|
||||
presetsFallback = { "BLUE", "GARY", "JOHN" },
|
||||
},
|
||||
{
|
||||
-- HisNameIsText, the rival's counterpart to the confirmation above
|
||||
id = "confirm_rival_name",
|
||||
kind = "say",
|
||||
textKey = "_HisNameIsText",
|
||||
},
|
||||
{
|
||||
id = "legend",
|
||||
kind = "say",
|
||||
@@ -611,6 +637,15 @@ function OakSpeech:draw()
|
||||
love.graphics.draw(self.walkSheet, self.walkQuad, 64, 60)
|
||||
end
|
||||
if self.shrinkText then
|
||||
-- This is a REPLICA of the dialogue box that just closed, redrawn at
|
||||
-- TextBox's own rect (BOX_TX..BOX_TH = 0,12,20,6) so the last page holds
|
||||
-- while the pic shrinks. The real box rides the bottom anchor, so this
|
||||
-- one has to as well -- otherwise the text visibly jumps up a letterbox
|
||||
-- on the frame the real box is swapped for this copy.
|
||||
local r = self.game and self.game.renderer
|
||||
if r and r.setUIAnchor then
|
||||
r:setUIAnchor(0, 12 * 8, 20 * 8, 6 * 8, "bottom")
|
||||
end
|
||||
Font.drawBox(0, 12, 20, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
for i, line in ipairs(self.shrinkText) do
|
||||
|
||||
@@ -19,6 +19,7 @@ local TileRenderer = require("src.render.TileRenderer")
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local VideoMode = require("src.core.VideoMode")
|
||||
local FaithfulRes = require("src.core.FaithfulRes")
|
||||
local FrameCap = require("src.core.FrameCap")
|
||||
local Performance = require("src.core.Performance")
|
||||
local Logger = require("src.core.Logger")
|
||||
@@ -156,6 +157,57 @@ local function buildRows(game)
|
||||
o.battleLayout = o.battleLayout == "wide" and "og" or "wide"
|
||||
return true
|
||||
end },
|
||||
-- FIXED keeps the classic integer-scaled letterbox -- a GB pixel is a
|
||||
-- whole number of screen pixels and the battle is the same size at any
|
||||
-- zoom. FILL scales the battle surface to the window so it fills
|
||||
-- vertically; that needs a fractional scale, so pixels stop being evenly
|
||||
-- sized. Battle only: the overworld is untouched either way.
|
||||
{ id = "battleFit", label = Strings("BATTLE SIZE"),
|
||||
value = function(g)
|
||||
return g.save.options.battleFit == "fill" and Strings("FILL")
|
||||
or Strings("FIXED")
|
||||
end,
|
||||
step = function(g)
|
||||
local o = g.save.options
|
||||
o.battleFit = o.battleFit == "fill" and "fixed" or "fill"
|
||||
return true
|
||||
end },
|
||||
-- What sits behind and around the battle. WHITE is the classic paper
|
||||
-- field; BLACK swaps it for black bars; WORLD leaves the frozen overworld
|
||||
-- visible underneath, dimmed (the battle stops being opaque, so the map
|
||||
-- shows through everywhere the battle does not paint).
|
||||
{ id = "battleBg", label = Strings("BATTLE BG"),
|
||||
value = function(g)
|
||||
local m = g.save.options.battleBg
|
||||
if m == "black" then return Strings("BLACK") end
|
||||
if m == "world" then return Strings("WORLD") end
|
||||
return Strings("WHITE")
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
local order = { "white", "black", "world" }
|
||||
local cur = 1
|
||||
for i, m in ipairs(order) do if o.battleBg == m then cur = i break end end
|
||||
o.battleBg = order[(cur - 1 + (dir or 1)) % #order + 1]
|
||||
return true
|
||||
end },
|
||||
-- CENTERED is a fixed letterbox: elements stay inside the 160x144 canvas
|
||||
-- and the UI does not follow the survey zoom, so nothing moves or resizes
|
||||
-- under the player. The composition the port shipped with. DYNAMIC docks
|
||||
-- the dialogue box to the window's bottom edge and the START menu to its
|
||||
-- top right, and steps the UI down with the zoom -- easier to read zoomed
|
||||
-- out, but it moves furniture the original never moved, so it is opt-in.
|
||||
-- BATTLE SIZE is independent of this and works under either.
|
||||
{ id = "uiLayout", label = Strings("UI LAYOUT"),
|
||||
value = function(g)
|
||||
return g.save.options.uiLayout == "dynamic" and Strings("DYNAMIC")
|
||||
or Strings("CENTERED")
|
||||
end,
|
||||
step = function(g)
|
||||
local o = g.save.options
|
||||
o.uiLayout = o.uiLayout == "dynamic" and "centered" or "dynamic"
|
||||
return true
|
||||
end },
|
||||
{ id = "ruleset", label = Strings("RULESET"),
|
||||
value = function(g) return rulesetName(g) end,
|
||||
step = function(g, dir)
|
||||
@@ -298,6 +350,20 @@ local function buildRows(game)
|
||||
VideoMode.apply(o.videoMode)
|
||||
return true
|
||||
end },
|
||||
-- Lock the window to an exact 160x144 multiple, so the surface IS the
|
||||
-- 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 RATIO"),
|
||||
value = function(g)
|
||||
return FaithfulRes.label(g.save.options.faithfulRes)
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
o.faithfulRes = FaithfulRes.cycle(o.faithfulRes, dir)
|
||||
FaithfulRes.apply(o.faithfulRes)
|
||||
return true
|
||||
end },
|
||||
-- hard render cap (issue #88): bounds the present rate so a
|
||||
-- driver-forced vsync-off run cannot spin at thousands of FPS. Logic
|
||||
-- is fixed-step off dt, so this touches presentation only.
|
||||
|
||||
+11
-3
@@ -373,9 +373,17 @@ function PartyMenu:update(dt)
|
||||
or Strings("A blinding FLASH\nlights the area!"), function()
|
||||
self:close()
|
||||
-- setDark, not a bare field write: ADVANCED carries the darkness
|
||||
-- in a baked atlas, so lighting the cave rebuilds it (#383)
|
||||
self.game.stack:push(Transition.whiteFlash(self.game, nil,
|
||||
function() ow:setDark(false) end))
|
||||
-- in a baked atlas, so lighting the cave drops every resident map
|
||||
-- and rebakes this one (#383). It runs HERE, before the blink,
|
||||
-- because start_sub_menus.asm .flash clears wMapPalOffset before
|
||||
-- PrintText and blinks last of all: the cave is already lit by the
|
||||
-- time GBPalWhiteOutWithDelay3 runs. Hanging the rebuild off the
|
||||
-- blink's completion instead left that rebuild's whole cost --
|
||||
-- seconds of per-pixel atlas baking on a phone -- on screen as a
|
||||
-- solid white frame with nothing under it, which reads as a
|
||||
-- lockup (#610).
|
||||
ow:setDark(false)
|
||||
self.game.stack:push(Transition.whiteFlash(self.game))
|
||||
end))
|
||||
return
|
||||
elseif action == "surf" then
|
||||
|
||||
+25
-14
@@ -53,7 +53,6 @@ function StartMenu.new(game)
|
||||
-- (PrintSaveScreenText)
|
||||
table.insert(items, { label = Strings("SAVE"), onSelect = function()
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local badges = require("src.inventory.Badges").count(game.data, game.save)
|
||||
local owned = 0
|
||||
for _ in pairs(game.save.pokedex and game.save.pokedex.owned or {}) do
|
||||
@@ -64,8 +63,8 @@ function StartMenu.new(game)
|
||||
game.save.player.name or "RED", badges, owned,
|
||||
math.floor(t / 3600), math.floor(t / 60) % 60)
|
||||
game.stack:push(TextBox.new(game,
|
||||
panel .. Strings("\fWould you like to\nSAVE the game?"), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
panel .. Strings("\fWould you like to\nSAVE the game?"), nil, {
|
||||
choice = function(yes)
|
||||
if not yes then return end
|
||||
-- "Now saving..." beat before the write (save.asm
|
||||
-- NowSavingString), then GameSavedText + SFX_SAVE
|
||||
@@ -75,8 +74,8 @@ function StartMenu.new(game)
|
||||
game.stack:push(TextBox.new(game,
|
||||
Strings("%s saved\nthe game!", game.save.player.name or "RED")))
|
||||
end))
|
||||
end))
|
||||
end))
|
||||
end,
|
||||
}))
|
||||
end })
|
||||
|
||||
table.insert(items, { label = Strings("OPTION"), onSelect = function()
|
||||
@@ -105,12 +104,12 @@ function StartMenu.new(game)
|
||||
-- to the title after a confirm (defaultNo guards accidental quits)
|
||||
table.insert(items, { label = Strings("QUIT"), onSelect = function()
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(TextBox.new(game, Strings("RETURN TO MAIN\nMENU?"), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
game.stack:push(TextBox.new(game, Strings("RETURN TO MAIN\nMENU?"), nil, {
|
||||
defaultNo = true,
|
||||
choice = function(yes)
|
||||
if yes then game:returnToTitle() end
|
||||
end, { defaultNo = true }))
|
||||
end))
|
||||
end,
|
||||
}))
|
||||
end })
|
||||
|
||||
local hooked = Runtime.call("ui.start_menu.items", sameItems, game, items)
|
||||
@@ -133,7 +132,12 @@ function StartMenu.new(game)
|
||||
local rowStep = 2
|
||||
local maxVisible = math.floor((Renderer.HEIGHT / 8 - 2) / rowStep)
|
||||
local menu = Menu.new(game, items,
|
||||
{ tx = 9, ty = 0, tw = 11, maxVisible = maxVisible, startCloses = true })
|
||||
-- the START menu hugs the top-right corner of the SCREEN, not of a
|
||||
-- centred letterbox: at 9,0 x 11 it is already flush with the top and
|
||||
-- right of the 20x18 grid, so the anchor keeps it flush when the view
|
||||
-- is zoomed out and the letterbox no longer fills the window
|
||||
{ tx = 9, ty = 0, tw = 11, maxVisible = maxVisible, startCloses = true,
|
||||
anchor = "topright" })
|
||||
-- the cursor position survives closing the menu
|
||||
-- (wBattleAndStartSavedMenuItem, home/start_menu.asm)
|
||||
menu.index = math.min(game.save.startMenuIndex or 1, #items)
|
||||
@@ -145,9 +149,16 @@ function StartMenu.new(game)
|
||||
end
|
||||
|
||||
-- inside the Safari Zone the start menu also shows remaining steps and
|
||||
-- SAFARI BALLs (PrintSafariZoneSteps): a 9x5 border at the top-left with
|
||||
-- "steps/500" and "BALL xx"
|
||||
if game.save.safari then
|
||||
-- SAFARI BALLs (PrintSafariZoneSteps, engine/overworld/player_state.asm:
|
||||
-- 219-224): a 9x5 border at the top-left with "steps/500" and "BALL xx".
|
||||
-- It opens with `cp SAFARI_ZONE_EAST / ret c`, so only the nine interior
|
||||
-- maps ($D9..$E1) get it -- SAFARI_ZONE_GATE is $9C and falls under that
|
||||
-- early out, and used to show "502/500" while the player was still
|
||||
-- standing at the counter (#540). Same map set as the step counter's
|
||||
-- (FieldDefaults safari.stepMaps via OverworldState:inSafariStepZone).
|
||||
local ow = game.overworld
|
||||
if game.save.safari and ow and ow.map and ow.inSafariStepZone
|
||||
and ow:inSafariStepZone() then
|
||||
local baseDraw = menu.draw
|
||||
menu.draw = function(self)
|
||||
baseDraw(self)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,6 +14,12 @@ local TitleState = {}
|
||||
TitleState.__index = TitleState
|
||||
TitleState.isOpaque = true
|
||||
|
||||
-- Fill the window (aspect preserved, bars on the long axis) instead of sitting
|
||||
-- at the fixed integer scale. The title screen is a full-bleed picture with no
|
||||
-- world behind it, so a small centred box in a large window is just wasted
|
||||
-- glass -- and unlike the overworld it has no zoom the player chose to respect.
|
||||
function TitleState:wantsFillScale() return true end
|
||||
|
||||
-- SGB title zones (PalPacket_Titlescreen): the logo rows get LOGO2,
|
||||
-- the version-ribbon band LOGO1, the rest MEWMON.
|
||||
--
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
-- Launcher touch-controls editor (#327): drag each on-screen button to a
|
||||
-- new spot, toggle the overlay off entirely, Reset to defaults, Done to
|
||||
-- persist into options.lua. Opened from the game panel's "Touch Controls"
|
||||
-- button; main.lua suspends the launcher the same way it does for the
|
||||
-- save editor.
|
||||
-- new spot, resize them, toggle the overlay off entirely, Reset to
|
||||
-- defaults, Done to persist into options.lua. Opened from the game
|
||||
-- panel's "Touch Controls" button; main.lua suspends the launcher the same
|
||||
-- way it does for the save editor.
|
||||
--
|
||||
-- Everything edited here belongs to the orientation currently on screen
|
||||
-- (#633): portrait and landscape keep separate positions and sizes, so a
|
||||
-- player lays out each rotation once. Rotate the device (or resize the
|
||||
-- desktop window past square) and the editor switches buckets live.
|
||||
--
|
||||
-- Draws in full window LOVE units -- the same space TouchControls uses
|
||||
-- after Renderer:endFrame -- so what you drag here is what you get in play.
|
||||
@@ -63,9 +68,11 @@ end
|
||||
local function persist()
|
||||
local opts = SaveData.loadOptions()
|
||||
local cfg = TouchControls:config()
|
||||
-- replaces the whole table, so a pre-#633 top-level positions key is
|
||||
-- dropped once the player saves under the new shape
|
||||
opts.touchControls = {
|
||||
enabled = cfg.enabled,
|
||||
positions = cfg.positions,
|
||||
layouts = cfg.layouts,
|
||||
}
|
||||
SaveData.saveOptions(opts)
|
||||
end
|
||||
@@ -113,6 +120,8 @@ function Editor.draw()
|
||||
local fullW, fullH = love.graphics.getDimensions()
|
||||
local ox, oy, ww, wh = SafeArea.rect()
|
||||
local s = math.max(0.75, math.min(1.4, wh / 768))
|
||||
-- resolve the orientation bucket before any chrome reads scale (#633)
|
||||
local bucket = TouchControls:currentBucket()
|
||||
Editor.rects = {}
|
||||
|
||||
-- radial-ish navy field (two stacked fills; matches launcher atmosphere)
|
||||
@@ -186,13 +195,41 @@ function Editor.draw()
|
||||
chromeBtn(toggle, on and "Disable" or "Enable",
|
||||
on and PAL.red or PAL.green)
|
||||
|
||||
-- size card (#633): -/+ resize every control in the orientation on
|
||||
-- screen; the heading names it so it is plain the other one is untouched
|
||||
local sizeY = cardY + cardH + 10 * s
|
||||
col(PAL.card, 0.88)
|
||||
roundRect("fill", cardX, sizeY, cardW, cardH, 12 * s)
|
||||
col(PAL.stroke, 0.4)
|
||||
roundRect("line", cardX, sizeY, cardW, cardH, 12 * s)
|
||||
|
||||
love.graphics.setFont(Editor.fonts.body)
|
||||
col(PAL.label)
|
||||
local orient = TouchControls.orientation == "landscape"
|
||||
and "Landscape" or "Portrait"
|
||||
love.graphics.print("Button size (" .. orient .. ")",
|
||||
cardX + 16 * s, sizeY + 12 * s)
|
||||
love.graphics.setFont(Editor.fonts.btn)
|
||||
col(PAL.white)
|
||||
love.graphics.print(
|
||||
string.format("%d%%", math.floor((bucket.scale or 1) * 100 + 0.5)),
|
||||
cardX + 16 * s, sizeY + 34 * s)
|
||||
|
||||
local stepW = 52 * s
|
||||
local plus = { x = cardX + cardW - 16 * s - stepW,
|
||||
y = sizeY + (cardH - btnH) / 2, w = stepW, h = btnH }
|
||||
local minus = { x = plus.x - 10 * s - stepW, y = plus.y, w = stepW, h = btnH }
|
||||
Editor.rects.sizeUp, Editor.rects.sizeDown = plus, minus
|
||||
chromeBtn(minus, "-", { 60, 70, 110 })
|
||||
chromeBtn(plus, "+", { 60, 70, 110 })
|
||||
|
||||
-- hint
|
||||
love.graphics.setFont(Editor.fonts.body)
|
||||
col(PAL.label, 0.9)
|
||||
local hint = on
|
||||
and "Drag each button to reposition. Layout is saved when you tap Done."
|
||||
and "Drag each button to reposition, -/+ to resize. Portrait and landscape are saved separately when you tap Done."
|
||||
or "Controls are hidden in-game. Enable them to show and edit the layout."
|
||||
love.graphics.printf(hint, ox + pad, cardY + cardH + 12 * s, ww - 2 * pad, "left")
|
||||
love.graphics.printf(hint, ox + pad, sizeY + cardH + 12 * s, ww - 2 * pad, "left")
|
||||
|
||||
-- the overlay itself (preview mode; dimmed when disabled)
|
||||
TouchControls:draw()
|
||||
@@ -214,6 +251,12 @@ local function beginDrag(id, x, y)
|
||||
if inside(Editor.rects.done, x, y) then close(); return end
|
||||
if inside(Editor.rects.reset, x, y) then resetLayout(); return end
|
||||
if inside(Editor.rects.toggle, x, y) then toggleEnabled(); return end
|
||||
if inside(Editor.rects.sizeDown, x, y) then
|
||||
TouchControls:nudgeScale(-TouchControls.SCALE_STEP); return
|
||||
end
|
||||
if inside(Editor.rects.sizeUp, x, y) then
|
||||
TouchControls:nudgeScale(TouchControls.SCALE_STEP); return
|
||||
end
|
||||
|
||||
local name = TouchControls:hitTest(x, y)
|
||||
if not name then return end
|
||||
@@ -271,6 +314,11 @@ function Editor.keypressed(key)
|
||||
close()
|
||||
elseif key == "r" then
|
||||
resetLayout()
|
||||
-- desktop shortcuts for the size -/+ (POKEPORT_TOUCH=1 testing, #633)
|
||||
elseif key == "-" or key == "kp-" then
|
||||
TouchControls:nudgeScale(-TouchControls.SCALE_STEP)
|
||||
elseif key == "=" or key == "+" or key == "kp+" then
|
||||
TouchControls:nudgeScale(TouchControls.SCALE_STEP)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user