mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-27 05:55:56 -04:00
02ad846dfa
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.
548 lines
21 KiB
Lua
548 lines
21 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), PIKACHU VOL (0-7, Yellow only: trims the PCM voice clips
|
|
-- under SFX VOL), 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 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")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local OptionRows = require("src.ui.OptionRows")
|
|
local Renderer = require("src.render.Renderer")
|
|
local Strings = require("src.core.Strings")
|
|
|
|
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 Strings(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 = Strings("TEXT SPEED"),
|
|
value = function(g) return Strings(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 = Strings("BATTLE ANIMATION"),
|
|
value = function(g)
|
|
return g.save.options.animations == false and Strings("OFF") or Strings("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 = Strings("BATTLE STYLE"),
|
|
value = function(g)
|
|
return g.save.options.battleStyle == "set" and Strings("SET") or Strings("SHIFT")
|
|
end,
|
|
step = function(g)
|
|
local o = g.save.options
|
|
o.battleStyle = o.battleStyle == "set" and "shift" or "set"
|
|
return true
|
|
end },
|
|
-- OG is the classic 160x144 battle screen; WIDE is the 304x144
|
|
-- widescreen composition (src/battle/WideBattle.lua)
|
|
{ id = "battleLayout", label = Strings("BATTLE LAYOUT"),
|
|
value = function(g)
|
|
return g.save.options.battleLayout == "wide" and Strings("WIDE") or Strings("OG")
|
|
end,
|
|
step = function(g)
|
|
local o = g.save.options
|
|
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)
|
|
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 = Strings("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 = Strings("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 },
|
|
-- Yellow only (filtered out below on Red/Blue): trims Pikachu's voice
|
|
-- clips under the SFX level, since Yellow plays them constantly (every
|
|
-- follower interaction, the title screen, the dex) and they are mixed
|
|
-- much hotter than the chip cries.
|
|
{ id = "pikaVol", label = Strings("PIKACHU VOL"),
|
|
value = function(g) return volLabel(g.save.options.pikaVol) end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.pikaVol = stepVolume(o.pikaVol, dir)
|
|
require("src.core.Sound").setPikaVolumeLevel(o.pikaVol)
|
|
return true
|
|
end },
|
|
{ id = "musicFilter", label = Strings("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 },
|
|
-- Heads the port's display group: one tier that scales the heavy extras
|
|
-- (TILT / GBC FX / survey ZOOM) and the FPS ceiling for weaker devices.
|
|
-- AUTO picks a default from the hardware; every tier is overridable.
|
|
-- Re-applies live so the extras clamp (or, on a higher tier, restore to
|
|
-- the player's stored TILT / GBC FX / ZOOM) the moment the row changes.
|
|
{ id = "performance", label = Strings("PERFORMANCE"),
|
|
value = function(g)
|
|
return Strings(Performance.label(g.save.options.performance))
|
|
end,
|
|
step = function(g, dir)
|
|
local o = g.save.options
|
|
o.performance = Performance.cycle(o.performance, dir)
|
|
g:applyOptions(o)
|
|
return true
|
|
end },
|
|
{ id = "colors", label = Strings("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 = Strings("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 = Strings("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 = Strings("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 = Strings("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 = Strings("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 },
|
|
-- 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.
|
|
{ id = "fpsCap", label = Strings("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 = Strings("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 = Strings("MODS"),
|
|
value = function(g)
|
|
local status = g.modStatus or {}
|
|
return Strings("%d INSTALLED", #(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 = Strings("CONTROLS"),
|
|
activate = function(g)
|
|
require("src.ui.Screens").push(g, "BindingsMenu")
|
|
end },
|
|
-- permanent on-screen pad toggle (#327); layout editing stays in the
|
|
-- launcher. Hidden where the overlay never appears (desktop without
|
|
-- POKEPORT_TOUCH), so the row costs a non-mobile install nothing.
|
|
{ id = "touchControls", label = Strings("TOUCH PAD"),
|
|
value = function(g)
|
|
local tc = g.save.options.touchControls
|
|
local on = not (type(tc) == "table" and tc.enabled == false)
|
|
return on and Strings("ON") or Strings("OFF")
|
|
end,
|
|
step = function(g)
|
|
local o = g.save.options
|
|
local tc = type(o.touchControls) == "table" and o.touchControls or {}
|
|
local on = not (tc.enabled == false)
|
|
tc.enabled = not on
|
|
-- keep any saved positions when toggling
|
|
o.touchControls = tc
|
|
require("src.core.TouchControls"):applyOptions(o)
|
|
return true
|
|
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
|
|
-- TOUCH PAD only where the overlay can appear (mobile, or desktop with
|
|
-- POKEPORT_TOUCH=1). POKEPORT_TOUCH=0 forces it off everywhere.
|
|
do
|
|
local env = os.getenv("POKEPORT_TOUCH")
|
|
local osName = love.system and love.system.getOS and love.system.getOS()
|
|
local show = env == "1"
|
|
or (env ~= "0" and (osName == "Android" or osName == "iOS"))
|
|
if not show then
|
|
local filtered = {}
|
|
for _, row in ipairs(rows) do
|
|
if row.id ~= "touchControls" then filtered[#filtered + 1] = row end
|
|
end
|
|
rows = filtered
|
|
end
|
|
end
|
|
-- PIKACHU VOL only means something where the voice clips exist: Yellow
|
|
-- (data.audio.pikaCries is the clip count the importer wrote). Red/Blue
|
|
-- keep the row list they always had.
|
|
local audio = game and game.data and game.data.audio
|
|
if not (GameVersion.isYellow() and audio and audio.pikaCries) then
|
|
local filtered = {}
|
|
for _, row in ipairs(rows) do
|
|
if row.id ~= "pikaVol" 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
|
|
-- DisplayOptionMenu .exitMenu (engine/menus/main_menu.asm) is the
|
|
-- only spot in this menu that plays SFX_PRESS_AB: A on a setting row
|
|
-- and the Left/Right toggles stay silent (#570). game.data is nil
|
|
-- under the stub games the UI harnesses drive.
|
|
if self.game.data then
|
|
require("src.core.Sound").play(self.game.data, "Press_AB")
|
|
end
|
|
self.game.stack:pop()
|
|
if self.onCancel then self.onCancel() end
|
|
end
|
|
elseif input:wasPressed("b") or input:wasPressed("start") then
|
|
-- .exitMenu again: B and START leave the same way, sound and all
|
|
if self.game.data then
|
|
require("src.core.Sound").play(self.game.data, "Press_AB")
|
|
end
|
|
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
|