mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
531a719794
Selecting a start-menu row pops the start menu before the submenu is pushed (Menu's generic A handler), so B in a submenu had no parent to return to and dropped straight to the overworld. pokered redisplays the start menu instead (RedisplayStartMenu from the party/item/ trainer-card/option handlers), so give each vanilla submenu an onCancel that re-opens the start menu; the saved cursor row (wBattleAndStartSavedMenuItem) restores on re-entry. - PokedexMenu/BagMenu forward opts.onCancel into their ListMenu - TrainerCard dismisses back via onCancel on A or B (WaitForTextScrollButtonPress then RedisplayStartMenu) - OptionsMenu fires onCancel from both B/START and the CANCEL row - PlayerPC rows are keepOpen so B in WITHDRAW/DEPOSIT/TOSS returns to the PC root menu (players_pc.asm), matching the BoxMenu pattern The SAVE flow is untouched: StartMenu_SaveReset falls through to HoldTextDisplayOpen and never redisplays the menu. Battle-opened party/bag screens get no onCancel, so mid-battle cancel behavior is unchanged. Co-authored-by: johnjohto <johnjohto@users.noreply.github.com>
384 lines
14 KiB
Lua
384 lines
14 KiB
Lua
-- Options: text speed, battle animation on/off, battle style SHIFT/SET
|
|
-- (engine/menus/main_menu.asm DisplayOptionMenu), the battle ruleset
|
|
-- (cycles the merged rulesets registry; gen1_faithful keeps the original
|
|
-- quirks), plus the port's audio rows and display rows: music/SFX
|
|
-- volume (0-7), music low-pass filter (OFF/1X/2X/3X), COLORS / TILT /
|
|
-- GBC FX / ZOOM / VOID FILL / VIDEO MODE, and the MODS row that opens
|
|
-- the mod manager.
|
|
-- Rows are descriptors fed through the ui.options.rows hook, so mods can
|
|
-- add their own; CANCEL is appended after the hook and stays fixed on the
|
|
-- bottom line like pokered's.
|
|
|
|
local PaletteFX = require("src.render.PaletteFX")
|
|
local Pipelines = require("src.render.Pipelines")
|
|
local Tilt = require("src.render.Tilt")
|
|
local GBCFX = require("src.render.GBCFX")
|
|
local Zoom = require("src.render.Zoom")
|
|
local TileRenderer = require("src.render.TileRenderer")
|
|
local GameSpeed = require("src.core.GameSpeed")
|
|
local VideoMode = require("src.core.VideoMode")
|
|
local FrameCap = require("src.core.FrameCap")
|
|
local Logger = require("src.core.Logger")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local OptionRows = require("src.ui.OptionRows")
|
|
local Renderer = require("src.render.Renderer")
|
|
|
|
local OptionsMenu = {}
|
|
OptionsMenu.__index = OptionsMenu
|
|
OptionsMenu.isOpaque = true
|
|
|
|
-- Opaque full-screen menu: own MEWMON so opening OPTION from the title
|
|
-- (or over the overworld) does not inherit TitleState's LOGO1 band -- that
|
|
-- zone covers UI rows 8-9, which is the third options box label line
|
|
-- (pink "MODS" strip when Blue's ROM LOGO1 white is {255,239,255}).
|
|
function OptionsMenu:sgbPalettes(game)
|
|
return PaletteFX.wholeNamed(game.data, "MEWMON")
|
|
end
|
|
|
|
-- TextSpeedOptionData frame delays with the original labels
|
|
local SPEEDS = { { 1, "FAST" }, { 3, "MEDIUM" }, { 5, "SLOW" } }
|
|
-- no-loader fallback for the ruleset row, same pair BattleState keeps
|
|
local Rulesets = {
|
|
gen1_faithful = require("src.battle.rulesets.gen1_faithful"),
|
|
modern_clean = require("src.battle.rulesets.modern_clean"),
|
|
}
|
|
local FILTERS = { "OFF", "1X", "2X", "3X" }
|
|
|
|
local function speedIndex(game)
|
|
-- default matches InitOptions' TEXT_DELAY_MEDIUM in wOptions
|
|
local cur = game.save.options.textSpeed or 3
|
|
for i, s in ipairs(SPEEDS) do
|
|
if s[1] == cur then return i end
|
|
end
|
|
return 2 -- MEDIUM
|
|
end
|
|
|
|
-- the ruleset row cycles the sorted non-hidden ids of the merged
|
|
-- registry (07-battle-extensibility.md 4.6), so mod-registered
|
|
-- rulesets are selectable; hidden marks a total conversion's exclusions
|
|
local function rulesetIds(game)
|
|
local rulesets = game.data and game.data.rulesets or Rulesets
|
|
local ids = {}
|
|
for id, record in pairs(rulesets) do
|
|
if not record.hidden then ids[#ids + 1] = id end
|
|
end
|
|
table.sort(ids)
|
|
return ids
|
|
end
|
|
|
|
local function rulesetIndex(game, ids)
|
|
local constants = game.data and game.data.constants
|
|
local cur = game.save.options.ruleset
|
|
or (constants and constants.defaultRuleset) or "gen1_faithful"
|
|
for i, id in ipairs(ids) do
|
|
if id == cur then return i end
|
|
end
|
|
return 1
|
|
end
|
|
|
|
local function rulesetName(game)
|
|
local rulesets = game.data and game.data.rulesets or Rulesets
|
|
local ids = rulesetIds(game)
|
|
local id = ids[rulesetIndex(game, ids)] or game.save.options.ruleset
|
|
local record = id and rulesets[id]
|
|
return record and record.name or id or "----"
|
|
end
|
|
|
|
-- 0-7 volume level display (0 = OFF)
|
|
local function volLabel(v)
|
|
v = v or 7
|
|
return v == 0 and "OFF" or tostring(v)
|
|
end
|
|
|
|
-- volume rows clamp at the ends, like pokered's text-speed cursor
|
|
-- (.pressedLeftInTextSpeed stays at FAST rather than wrapping)
|
|
local function stepVolume(v, dir)
|
|
return math.max(0, math.min(7, (v or 7) + dir))
|
|
end
|
|
|
|
local function colorIndex(opts)
|
|
local cur = opts.colors or "gbc"
|
|
for i, m in ipairs(PaletteFX.MODES) do
|
|
if m == cur then return i end
|
|
end
|
|
return 1
|
|
end
|
|
|
|
local function wrapIndex(i, n)
|
|
i = i % n
|
|
if i < 0 then i = i + n end
|
|
return i
|
|
end
|
|
|
|
local function sameRows(_, rows) return rows end
|
|
|
|
-- the vanilla rows as descriptors; each step body is the old per-index
|
|
-- ladder's, so the save.options mutations are unchanged
|
|
local function buildRows(game)
|
|
local rows = {
|
|
{ id = "textSpeed", label = "TEXT SPEED",
|
|
value = function(g) return SPEEDS[speedIndex(g)][2] end,
|
|
step = function(g)
|
|
local i = speedIndex(g) % #SPEEDS + 1
|
|
g.save.options.textSpeed = SPEEDS[i][1]
|
|
return true
|
|
end },
|
|
{ id = "animations", label = "BATTLE ANIMATION",
|
|
value = function(g)
|
|
return g.save.options.animations == false and "OFF" or "ON"
|
|
end,
|
|
step = function(g)
|
|
local o = g.save.options
|
|
o.animations = o.animations == false and true or false
|
|
return true
|
|
end },
|
|
{ id = "battleStyle", label = "BATTLE STYLE",
|
|
value = function(g)
|
|
return g.save.options.battleStyle == "set" and "SET" or "SHIFT"
|
|
end,
|
|
step = function(g)
|
|
local o = g.save.options
|
|
o.battleStyle = o.battleStyle == "set" and "shift" or "set"
|
|
return true
|
|
end },
|
|
{ id = "ruleset", label = "RULESET",
|
|
value = function(g) return rulesetName(g) end,
|
|
step = function(g, dir)
|
|
local ids = rulesetIds(g)
|
|
if #ids == 0 then return false end
|
|
local i = rulesetIndex(g, ids)
|
|
g.save.options.ruleset = ids[wrapIndex(i - 1 + dir, #ids) + 1]
|
|
return true
|
|
end },
|
|
{ id = "musicVol", label = "MUSIC VOL",
|
|
value = function(g) return volLabel(g.save.options.musicVol) end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.musicVol = stepVolume(o.musicVol, dir)
|
|
require("src.core.Music").setVolumeLevel(o.musicVol)
|
|
return true
|
|
end },
|
|
{ id = "sfxVol", label = "SFX VOL",
|
|
value = function(g) return volLabel(g.save.options.sfxVol) end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.sfxVol = stepVolume(o.sfxVol, dir)
|
|
require("src.core.Sound").setVolumeLevel(o.sfxVol)
|
|
return true
|
|
end },
|
|
{ id = "musicFilter", label = "MUSIC FILTER",
|
|
value = function(g)
|
|
return FILTERS[(g.save.options.musicFilter or 0) + 1]
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.musicFilter = ((o.musicFilter or 0) + dir) % #FILTERS
|
|
require("src.core.Music").setFilterLevel(o.musicFilter)
|
|
return true
|
|
end },
|
|
{ id = "colors", label = "COLORS",
|
|
value = function(g)
|
|
return PaletteFX.modeLabel(g.save.options.colors or "gbc")
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
local i = colorIndex(o)
|
|
i = wrapIndex(i - 1 + dir, #PaletteFX.MODES) + 1
|
|
o.colors = PaletteFX.MODES[i]
|
|
PaletteFX.setMode(o.colors)
|
|
return true
|
|
end },
|
|
{ id = "tilt", label = "TILT",
|
|
value = function(g) return Tilt.levelLabel(g.save.options.tilt or 0) end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.tilt = wrapIndex((o.tilt or 0) + dir, 4)
|
|
Tilt.setLevel(o.tilt)
|
|
-- tilt and a mod's world pipeline are two answers to the same
|
|
-- question; turning this on switches that off (Pipelines does the
|
|
-- same in the other direction)
|
|
if o.tilt > 0 then
|
|
for _, entry in ipairs(Pipelines.list()) do
|
|
if entry.def.drawWorld then Pipelines.setLevel(entry.id, 0) end
|
|
end
|
|
Pipelines.syncOptions(o)
|
|
end
|
|
return true
|
|
end },
|
|
{ id = "gbcfx", label = "GBC FX",
|
|
value = function(g)
|
|
return GBCFX.levelLabel(g.save.options.gbcfx or 0)
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.gbcfx = wrapIndex((o.gbcfx or 0) + dir, 5)
|
|
GBCFX.setLevel(o.gbcfx)
|
|
return true
|
|
end },
|
|
{ id = "zoom", label = "ZOOM",
|
|
value = function(g)
|
|
return Zoom.offsetLabel(g.save.options.zoom or 0)
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
local S = Renderer:fitScale()
|
|
local lo, hi = Zoom.offsetRange(S)
|
|
local off = (o.zoom or 0) + dir
|
|
if off > hi then off = lo
|
|
elseif off < lo then off = hi end
|
|
o.zoom = off
|
|
Zoom.offset = off
|
|
return true
|
|
end },
|
|
{ id = "voidFill", label = "VOID FILL",
|
|
value = function(g)
|
|
return TileRenderer.voidFillLabel(g.save.options.voidFill)
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
local modes = TileRenderer.VOID_FILLS
|
|
local cur = o.voidFill or "trees"
|
|
local i = 1
|
|
for idx, m in ipairs(modes) do
|
|
if m == cur then i = idx; break end
|
|
end
|
|
o.voidFill = modes[wrapIndex(i - 1 + dir, #modes) + 1]
|
|
TileRenderer.setVoidFill(o.voidFill)
|
|
return true
|
|
end },
|
|
{ id = "videoMode", label = "VIDEO MODE",
|
|
value = function(g)
|
|
return VideoMode.modeLabel(g.save.options.videoMode)
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.videoMode = VideoMode.cycle(o.videoMode, dir)
|
|
VideoMode.apply(o.videoMode)
|
|
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.
|
|
{ id = "fpsCap", label = "MAX FPS",
|
|
value = function(g)
|
|
return FrameCap.label(g.save.options.fpsCap)
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.fpsCap = FrameCap.cycle(o.fpsCap, dir)
|
|
FrameCap.apply(o.fpsCap)
|
|
return true
|
|
end },
|
|
-- fast-forward the logic clock only; music and sfx keep their tempo
|
|
-- (src/core/GameSpeed.lua), so this is safe to leave on
|
|
{ id = "speed", label = "GAME SPEED",
|
|
value = function(g)
|
|
return GameSpeed.levelLabel(g.save.options.speed)
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.speed = GameSpeed.cycle(o.speed, dir)
|
|
return true
|
|
end },
|
|
-- the manager's discoverable home (18-mod-manager-ux); inert until
|
|
-- opened, so the row costs a vanilla install nothing
|
|
{ id = "mods", label = "MODS",
|
|
value = function(g)
|
|
local status = g.modStatus or {}
|
|
return ("%d INSTALLED"):format(#(status.available or {}))
|
|
end,
|
|
activate = function(g)
|
|
require("src.ui.Screens").push(g, "ManagerState")
|
|
end },
|
|
-- rebinding UI (gap C2, 12-ui-extensibility 4.4); captured inputs
|
|
-- live in options.bindings, so the row costs a vanilla install nothing
|
|
{ id = "controls", label = "CONTROLS",
|
|
activate = function(g)
|
|
require("src.ui.Screens").push(g, "BindingsMenu")
|
|
end },
|
|
}
|
|
-- issue #136: hide GBC FX on Android/iOS -- the present shader soft-bricks
|
|
if not GBCFX.isSupported() then
|
|
local filtered = {}
|
|
for _, row in ipairs(rows) do
|
|
if row.id ~= "gbcfx" then filtered[#filtered + 1] = row end
|
|
end
|
|
rows = filtered
|
|
end
|
|
-- A mod's render pipelines are display modes like TILT, so their rows sit
|
|
-- with it rather than at the end of the list where a mod's own
|
|
-- ui.options.rows additions land. Nothing registered means nothing
|
|
-- spliced, so a vanilla install sees the list it always had.
|
|
local pipelineRows = Pipelines.rows(game)
|
|
if pipelineRows[1] then
|
|
local merged = {}
|
|
for _, row in ipairs(rows) do
|
|
merged[#merged + 1] = row
|
|
if row.id == "tilt" then
|
|
for _, extra in ipairs(pipelineRows) do merged[#merged + 1] = extra end
|
|
end
|
|
end
|
|
-- no TILT row to anchor to (a future build could drop it): append
|
|
-- rather than silently lose the modes
|
|
if #merged == #rows then
|
|
for _, extra in ipairs(pipelineRows) do merged[#merged + 1] = extra end
|
|
end
|
|
rows = merged
|
|
end
|
|
return rows
|
|
end
|
|
|
|
function OptionsMenu.new(game, opts)
|
|
opts = opts or {}
|
|
local rows = buildRows(game)
|
|
local hooked = Runtime.call("ui.options.rows", sameRows, game, rows)
|
|
if type(hooked) == "table" then
|
|
rows = hooked
|
|
else
|
|
Logger.error("ui.options.rows returned %s; keeping the vanilla rows",
|
|
type(hooked))
|
|
end
|
|
return setmetatable({ game = game, rows = rows, index = 1, scroll = 0,
|
|
onCancel = opts.onCancel }, OptionsMenu)
|
|
end
|
|
|
|
function OptionsMenu:update(dt)
|
|
local input = self.game.input
|
|
local rows = self.rows
|
|
-- CANCEL sits below the hook-built rows so a mod cannot orphan the exit
|
|
local cancelRow = #rows + 1
|
|
local changed = false
|
|
if input:wasPressed("up") then
|
|
self.index = self.index > 1 and self.index - 1 or cancelRow
|
|
elseif input:wasPressed("down") then
|
|
self.index = self.index < cancelRow and self.index + 1 or 1
|
|
elseif input:wasPressed("left") or input:wasPressed("right")
|
|
or input:wasPressed("a") then
|
|
local dir = input:wasPressed("left") and -1 or 1
|
|
local row = rows[self.index]
|
|
if row and row.activate then
|
|
if input:wasPressed("a") then row.activate(self.game) end
|
|
elseif row and row.step then
|
|
changed = row.step(self.game, dir) and true or false
|
|
elseif input:wasPressed("a") then -- CANCEL
|
|
self.game.stack:pop()
|
|
if self.onCancel then self.onCancel() end
|
|
end
|
|
elseif input:wasPressed("b") or input:wasPressed("start") then
|
|
self.game.stack:pop()
|
|
if self.onCancel then self.onCancel() end
|
|
end
|
|
if changed and self.game.writeOptions then
|
|
self.game:writeOptions()
|
|
end
|
|
self.scroll = OptionRows.clampScroll(self.index, self.scroll or 0,
|
|
#rows, cancelRow)
|
|
end
|
|
|
|
function OptionsMenu:draw()
|
|
OptionRows.draw(self.game, self.rows, self.index, self.scroll or 0,
|
|
"CANCEL", #self.rows + 1)
|
|
end
|
|
|
|
return OptionsMenu
|