CLOSES #2578, CLOSES #2617, CLOSES #2623

This commit is contained in:
bryanthaboi
2026-10-02 13:52:21 -04:00
parent 22586020ff
commit 178cbf1bc4
75 changed files with 1001 additions and 106 deletions
+1 -12
View File
@@ -545,22 +545,11 @@ function Game.wideBattleInStack(stack)
return nil
end
-- Which of "battle"/"overworld"/"menu" per-category GAME SPEED (RFC 0007)
-- applies right now. Whole-stack, the same idiom as fillScaleInStack/
-- wideBattleInStack above: an overlay with neither marker (PartyMenu,
-- ChoiceBox, a NamingScreen, a text box) is transparent to the walk and
-- inherits whatever is under it, making the category a property of the
-- STACK POSITION the overlay sits over, not of the overlay itself. A
-- scripted sequence (script.started/ended) never pushes a state of its
-- own either -- it runs through the owning overworld/battle state's own
-- script runner or message queue -- so it inherits the same way. Nothing
-- identifying as either (the title screen, credits, an intro cutscene
-- with nothing under it) falls to "menu", the bucket every non-gameplay
-- screen gets; see the RFC's Decisions section for the full reasoning.
function Game.speedCategoryInStack(stack)
local states = stack and stack.states
for i = #(states or {}), 1, -1 do
local state = states[i]
if state and state.isMenu then return "menu" end
if state and state.isBattle then return "battle" end
if state and state.isOverworld then return "overworld" end
end
+1
View File
@@ -2462,6 +2462,7 @@ function Game2:applyOptions()
hotbar = options.hotbar,
})
require("src.core.VideoMode").applyOptions(options)
require("src.core.Orientation").applyOptions(options)
require("src.core.FaithfulRes").applyOptions(options)
require("src.core.ScreenPosition").applyOptions(options)
require("src.core.VSync").applyOptions(options)
+4
View File
@@ -604,6 +604,10 @@ function Game3:fixedUpdate(dt)
end
function Game3:speedCategory()
local Stack = lazyReq("src.ui.game3.stack")
for i = #(Stack._layers or {}), 1, -1 do
if Stack._layers[i].isMenu then return "menu" end
end
local okB, Battle = pcall(lazyReq, "src.core.game3.battle")
if okB and Battle and Battle.isActive and Battle.isActive() then
return "battle"
+7
View File
@@ -976,6 +976,13 @@ function Collision.isWater(cx, cy)
return Collision.isWaterOn(Collision._mapDef, cx, cy, Collision.cell(cx, cy))
end
-- pokeemerald/src/field_player_avatar.c:693
-- pokefirered/src/field_player_avatar.c:568
function Collision.isSurfDismount(cx, cy, elevation)
return Collision.elevationAt(cx, cy) == 3
and Collision.elevationMismatchOn(Collision._mapDef, elevation, cx, cy)
end
-- pokeemerald/src/rotating_gate.c:961
function Collision.rotatingGateCollision(game, dir, x, y)
local RG = package.loaded["src.core.game3.rotating_gate"]
+3 -8
View File
@@ -604,7 +604,7 @@ function Player.tryMove(dir, game, run)
return "blocked", "acro"
end
local isDismount = Player.surfing and not Player.underwater
and (not (Collision.isWater and Collision.isWater(tx, ty)))
and Collision.isSurfDismount(tx, ty, Player.currentElevation)
if isDismount then
Player.dismounting = true
lazyReq("src.core.game3.audio").stopSurfMusic()
@@ -747,8 +747,8 @@ function Player.forcedStep(dir, frames, opts)
beginStep(opts.ledgeX, opts.ledgeY, false, true)
else
local tx, ty = Player.cellX + d[1], Player.cellY + d[2]
Player.dismounting = Player.surfing
and (not (Collision.isWater and Collision.isWater(tx, ty))) or false
Player.dismounting = Player.surfing and not Player.underwater
and Collision.isSurfDismount(tx, ty, Player.currentElevation) or false
-- pokefirered/src/field_player_avatar.c:1609
if Player.dismounting then lazyReq("src.core.game3.audio").stopSurfMusic() end
beginStep(tx, ty, false, false)
@@ -931,11 +931,6 @@ local function finishStep(game)
elseif Player.dismounting then
Player.dismounting = false
Player.surfing = false
elseif Player.surfing then
local onWater = Collision.isWater and Collision.isWater(Player.cellX, Player.cellY)
if not onWater then
Player.surfing = false
end
end
local session = package.loaded["src.core.game3.runtime"]
+5 -2
View File
@@ -353,6 +353,7 @@ Save.DEFAULT_OPTIONS = {
haptics = "light",
touchControls = { enabled = true },
screenPos = "center",
orientation = "auto",
}
function Save.defaultOptions()
@@ -374,7 +375,7 @@ Save.OPTIONS_KEY = "gold"
local SHARED_KEYS = {
touchControls = true, haptics = true, screenPos = true,
videoMode = true, faithfulRes = true,
videoMode = true, faithfulRes = true, orientation = true,
mods = true, modsByVersion = true, modsGen2 = true,
modOptions = true, modProfiles = true, modProfilesSeeded = true,
activeProfile = true,
@@ -400,6 +401,7 @@ function Save.loadOptions(fs)
end
end
end
options.orientation = require("src.core.Orientation").normalize(options.orientation)
return options
end
@@ -413,7 +415,8 @@ function Save.saveOptions(options, fs)
local block = {}
for key, value in pairs(options) do
if SHARED_KEYS[key] then
file[key] = value
file[key] = key == "orientation"
and require("src.core.Orientation").normalize(value) or value
else
block[key] = value
end
+15 -15
View File
@@ -146,6 +146,19 @@ local function addTouchRows(rows, add, opts, hooks)
end
end
local function addOrientationRow(add, opts)
local okOr, Orientation = pcall(require, "src.core.Orientation")
if okOr and (Orientation.isAndroid() or Orientation.isIOS()) then
add(Strings("ORIENTATION"),
function() return Strings(Orientation.modeLabel(opts.orientation)) end,
function(dir)
opts.orientation = Orientation.cycle(opts.orientation, dir)
Orientation.apply(opts.orientation)
return true
end)
end
end
local function coreRows(opts, hooks)
local rows = {}
local function add(label, value, step)
@@ -295,21 +308,7 @@ local function coreRows(opts, hooks)
end)
end
-- ORIENTATION (#592, #1638): mobile only. Unlike the other launcher rows
-- this one live-applies: the window exists here too, and rotating under
-- the player's finger is the only feedback that reads.
do
local okOr, Orientation = pcall(require, "src.core.Orientation")
if okOr and (Orientation.isAndroid() or Orientation.isIOS()) then
add(Strings("ORIENTATION"),
function() return Strings(Orientation.modeLabel(opts.orientation)) end,
function(dir)
opts.orientation = Orientation.cycle(opts.orientation, dir)
Orientation.apply(opts.orientation)
return true
end)
end
end
addOrientationRow(add, opts)
local okFr, FaithfulRes = pcall(require, "src.core.FaithfulRes")
if okFr then
@@ -738,6 +737,7 @@ local function gen2Rows(opts, hooks, shared)
end)
addTouchRows(rows, add, shared, hooks)
addOrientationRow(add, shared)
return rows
end
+2 -1
View File
@@ -8,7 +8,7 @@ local Runtime = require("src.mods.Runtime")
local TextBox = require("src.render.TextBox")
local romText = require("src.core.RomText")
local BagMenu = {}
local BagMenu = { isMenu = true }
local Bag = require("src.inventory.Bag")
local Strings = require("src.core.Strings")
@@ -735,6 +735,7 @@ function BagMenu.new(game, opts)
-- the item box overlaps the kept-open START menu box, so neither docks to
-- a screen edge on its own (start_sub_menus.asm:302-329) #1745
list.holdsUIAnchors = true
list.isMenu = true
return list
end
+2 -1
View File
@@ -11,7 +11,7 @@ local Stats = require("src.pokemon.Stats")
local TextBox = require("src.render.TextBox")
local Strings = require("src.core.Strings")
local BoxMenu = {}
local BoxMenu = { isMenu = true }
-- engine/pokemon/bills_pc.asm:118
-- vChars2 $78; engine/menus/save.asm:497
@@ -396,6 +396,7 @@ function BoxMenu.new(game)
baseDraw(self)
drawChrome(game)
end
menu.isMenu = true
return menu
end
+1 -1
View File
@@ -16,7 +16,7 @@ local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local DexEntryMenu = {}
local DexEntryMenu = { isMenu = true }
DexEntryMenu.__index = DexEntryMenu
DexEntryMenu.isOpaque = true
+4 -2
View File
@@ -5,7 +5,7 @@ local ListMenu = require("src.ui.ListMenu")
local Map = require("src.world.Map")
local Strings = require("src.core.Strings")
local FlyMenu = {}
local FlyMenu = { isMenu = true }
function FlyMenu.new(game)
local items = {}
@@ -25,12 +25,14 @@ function FlyMenu.new(game)
})
end
end
return ListMenu.new(game, Strings.source("FLY TO?"), items, {
local menu = ListMenu.new(game, Strings.source("FLY TO?"), items, {
onChoose = function(item, list)
list:close()
game.overworld:flyTo(item.value)
end,
})
menu.isMenu = true
return menu
end
return FlyMenu
+1 -1
View File
@@ -4,7 +4,7 @@ local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local HallOfFame = require("src.ui.HallOfFame")
local LeaguePC = {}
local LeaguePC = { isMenu = true }
LeaguePC.__index = LeaguePC
LeaguePC.isOpaque = true
+1 -1
View File
@@ -17,7 +17,7 @@ local Sound = require("src.core.Sound")
local Theme = require("src.ui.Theme")
local Strings = require("src.core.Strings")
local NamingScreen = {}
local NamingScreen = { isMenu = true }
NamingScreen.__index = NamingScreen
NamingScreen.isOpaque = true
+1 -1
View File
@@ -34,7 +34,7 @@ local OptionRows = require("src.ui.OptionRows")
local Renderer = require("src.render.Renderer")
local Strings = require("src.core.Strings")
local OptionsMenu = {}
local OptionsMenu = { isMenu = true }
OptionsMenu.__index = OptionsMenu
OptionsMenu.isOpaque = true
+1 -1
View File
@@ -21,7 +21,7 @@ local Map = require("src.world.Map")
local Strings = require("src.core.Strings")
local Status = require("src.battle.Status")
local PartyMenu = {}
local PartyMenu = { isMenu = true }
PartyMenu.__index = PartyMenu
PartyMenu.isOpaque = true
+2 -1
View File
@@ -16,7 +16,7 @@ local romText = require("src.core.RomText")
local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local PlayerPC = {}
local PlayerPC = { isMenu = true }
local function itemName(game, id)
local def = game.data.items[id]
@@ -308,6 +308,7 @@ function PlayerPC.new(game, opts)
end
love.graphics.setColor(1, 1, 1, 1)
end
menu.isMenu = true
return menu
end
+1 -1
View File
@@ -6,7 +6,7 @@ local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local MenuRepeat = require("src.ui.MenuRepeat")
local PokedexMenu = {}
local PokedexMenu = { isMenu = true }
PokedexMenu.__index = PokedexMenu
PokedexMenu.isOpaque = true
+1 -1
View File
@@ -4,7 +4,7 @@ local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local PrizeCounter = {}
local PrizeCounter = { isMenu = true }
PrizeCounter.__index = PrizeCounter
local NAME_X, TOP_Y, ROW_STEP = 16, 32, 16
+1
View File
@@ -172,6 +172,7 @@ local function build(game, id, ...)
inst = factory.new(game, ...)
end
inst.screenId = inst.screenId or id
if factory.isMenu and inst.isMenu == nil then inst.isMenu = true end
-- Standardized opt-in marker for mod-created options/settings screens.
-- A mod may declare `isModOptions = true` on its screen factory table or
-- on the returned instance. Either way the flag is propagated so that other
+2 -1
View File
@@ -16,7 +16,7 @@ local Strings = require("src.core.Strings")
local TextBox = require("src.render.TextBox")
local romText = require("src.core.RomText")
local ShopMenu = {}
local ShopMenu = { isMenu = true }
local function txt(game, key, fallback)
return game.data.text[key] or fallback
@@ -343,6 +343,7 @@ function ShopMenu.new(game, stock, onQuit)
drawClerk(self)
Menu.draw(self)
end
menu.isMenu = true
return menu
end
+2 -1
View File
@@ -13,7 +13,7 @@ local Screens = require("src.ui.Screens")
local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local StartMenu = {}
local StartMenu = { isMenu = true }
local function sameItems(_, items) return items end
@@ -229,6 +229,7 @@ function StartMenu.new(game)
love.graphics.setColor(1, 1, 1, 1)
end
end
menu.isMenu = true
return menu
end
+1 -1
View File
@@ -39,7 +39,7 @@ do
end
end
local SummaryMenu = {}
local SummaryMenu = { isMenu = true }
SummaryMenu.__index = SummaryMenu
SummaryMenu.isOpaque = true
+1 -1
View File
@@ -21,7 +21,7 @@ local SpriteRenderer = require("src.render.SpriteRenderer")
local Strings = require("src.core.Strings")
local Theme = require("src.ui.Theme")
local TownMap = {}
local TownMap = { isMenu = true }
TownMap.__index = TownMap
TownMap.isOpaque = true
+1 -1
View File
@@ -9,7 +9,7 @@ local Badges = require("src.inventory.Badges")
local Font = require("src.render.Font")
local Strings = require("src.core.Strings")
local TrainerCard = {}
local TrainerCard = { isMenu = true }
TrainerCard.__index = TrainerCard
TrainerCard.isOpaque = true
+1 -1
View File
@@ -13,7 +13,7 @@ local PartyView = require("src.core.game3.battle.party_view")
local RomText = require("src.core.game3.rom_text")
local TextIR = require("src.core.game3.scripting.text_ir")
local BagMenu = {}
local BagMenu = { isMenu = true }
BagMenu.open = false
BagMenu.cursor = 1
+1 -1
View File
@@ -25,7 +25,7 @@ local ItemUse = require("src.core.game3.item_use")
local PartyView = require("src.core.game3.battle.party_view")
local RomText = require("src.core.game3.rom_text")
local BerryPouch = {}
local BerryPouch = { isMenu = true }
BerryPouch.open = false
BerryPouch.cursor = 1
+1 -1
View File
@@ -19,7 +19,7 @@ local SummaryMenu = require("src.ui.game3.summary_menu")
local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local BoxStorageUI = {}
local BoxStorageUI = { isMenu = true }
-- pokefirered/src/pokemon_storage_system_data.c:2027
local MENU_TEXT = {
+1 -1
View File
@@ -6,7 +6,7 @@ local Strings = require("src.core.Strings")
local BindingsMenu = require("src.ui.BindingsMenu")
local ChoiceBox = require("src.ui.ChoiceBox")
local Controls = {}
local Controls = { isMenu = true }
Controls.ID = "controls"
Controls.open = false
+1 -1
View File
@@ -3,7 +3,7 @@
local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local DaycareMenu = {}
local DaycareMenu = { isMenu = true }
local DAYCARE_MON_COUNT = 2 -- pokefirered/include/constants/global.h:34
local DAYCARE_LEVEL_MENU_EXIT = 5 -- pokefirered/include/constants/daycare.h:20
+1 -1
View File
@@ -10,7 +10,7 @@ local Chrome = require("src.ui.game3.chrome")
local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local EasyChat = {}
local EasyChat = { isMenu = true }
EasyChat.openFlag = false
EasyChat._state = nil
+1 -1
View File
@@ -7,7 +7,7 @@ local FameChecker = require("src.core.game3.fame_checker")
local TextIR = require("src.core.game3.scripting.text_ir")
local RomText = require("src.core.game3.rom_text")
local FameCheckerUi = {}
local FameCheckerUi = { isMenu = true }
local PERSON = FameChecker.PERSON
local PICK = FameChecker.PICKSTATE
+1 -1
View File
@@ -7,7 +7,7 @@ local Pokemon = require("src.core.game3.pokemon")
local RomText = require("src.core.game3.rom_text")
local HofGfx = require("src.ui.game3.hall_of_fame_gfx")
local HofPc = {}
local HofPc = { isMenu = true }
HofPc.open = false
HofPc._teams = {}
+1 -1
View File
@@ -7,7 +7,7 @@ local Storage = require("src.core.game3.storage")
local RomText = require("src.core.game3.rom_text")
local Trig = require("src.core.game3.trig")
local ItemPc = {}
local ItemPc = { isMenu = true }
ItemPc.open = false
ItemPc.mode = "list"
+1 -1
View File
@@ -11,7 +11,7 @@ local FrlgFont = require("src.ui.game3.frlg_font")
local Strings = require("src.core.Strings")
local ManagerState = require("src.mods.ManagerState")
local ModManager = {}
local ModManager = { isMenu = true }
ModManager.open = false
+1 -1
View File
@@ -11,7 +11,7 @@ local SummaryData = require("src.core.game3.summary_data")
local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local MoveRelearner = {}
local MoveRelearner = { isMenu = true }
MoveRelearner.open = false
MoveRelearner.state = "list"
+1 -1
View File
@@ -14,7 +14,7 @@ local Versions = require("src.import.gba.versions")
local RomText = require("src.core.game3.rom_text")
local TextIR = require("src.core.game3.scripting.text_ir")
local Naming = {}
local Naming = { isMenu = true }
Naming.MAX_LEN = 7
Naming.openFlag = false
+1 -1
View File
@@ -4,7 +4,7 @@ local Options = require("src.core.game3.options")
local Rows = require("src.ui.game3.option_rows")
local RomText = require("src.core.game3.rom_text")
local OptionMenu = {}
local OptionMenu = { isMenu = true }
OptionMenu.open = false
OptionMenu.cursor = 1
+1 -1
View File
@@ -16,7 +16,7 @@ local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local TextIR = require("src.core.game3.scripting.text_ir")
local PartyMenu = {}
local PartyMenu = { isMenu = true }
PartyMenu.open = false
PartyMenu.cursor = 1
+1 -1
View File
@@ -15,7 +15,7 @@ local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local Profile = require("src.core.game3.profile")
local PcMenu = {}
local PcMenu = { isMenu = true }
local function rse_pc()
if Profile.family(PcMenu._session) ~= "rse" then return nil end
+1 -1
View File
@@ -17,7 +17,7 @@ local PokedexChrome = require("src.ui.game3.pokedex_chrome")
local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local Pokedex = {}
local Pokedex = { isMenu = true }
Pokedex.open = false
Pokedex.screen = "mode_select"
+1 -1
View File
@@ -4,7 +4,7 @@ local Stack = require("src.ui.game3.stack")
local Window = require("src.ui.game3.window")
local FrlgFont = require("src.ui.game3.frlg_font")
local PrizeCorner = {}
local PrizeCorner = { isMenu = true }
-- pokefirered/include/constants/menu.h:21
PrizeCorner.LIST_POKEMON_PRIZES = 14
+1 -1
View File
@@ -8,7 +8,7 @@ local Strings = require("src.core.Strings")
local Gpu = require("src.ui.game3.region_map_gpu")
local Position = require("src.ui.game3.region_map_position")
local RegionMap = {}
local RegionMap = { isMenu = true }
RegionMap.open = false
RegionMap.cursorX = 0
+1 -1
View File
@@ -5,7 +5,7 @@ local Kit = require("src.ui.game3.rse.scene_kit")
local Gfx = require("src.ui.game3.rse.pokeblock_gfx")
local BerryTrees = require("src.core.game3.rse.berry_trees")
local Tag = {}
local Tag = { isMenu = true }
Tag.ID = "rse_berry_tag"
Tag.open = false
+1 -1
View File
@@ -5,7 +5,7 @@ local Kit = require("src.ui.game3.rse.scene_kit")
local Gfx = require("src.ui.game3.rse.pokeblock_gfx")
local PalFade = require("src.core.game3.pal_fade")
local Pass = {}
local Pass = { isMenu = true }
Pass.ID = "rse_frontier_pass"
Pass.MAP_ID = "rse_frontier_map"
+1 -1
View File
@@ -5,7 +5,7 @@ local RomText = require("src.core.game3.rom_text")
local Storage = require("src.core.game3.storage")
local Bag = require("src.core.game3.bag")
local ItemStorage = {}
local ItemStorage = { isMenu = true }
ItemStorage.open = false
+1 -1
View File
@@ -4,7 +4,7 @@ local FrlgFont = require("src.ui.game3.frlg_font")
local RomText = require("src.core.game3.rom_text")
local Mail = require("src.core.game3.mail")
local Mailbox = {}
local Mailbox = { isMenu = true }
Mailbox.open = false
+1 -1
View File
@@ -10,7 +10,7 @@ local SummaryData = require("src.core.game3.summary_data")
local RomText = require("src.core.game3.rom_text")
local Kit = require("src.ui.game3.rse.scene_kit")
local RseRelearner = {}
local RseRelearner = { isMenu = true }
RseRelearner.SUB = "rse/move_relearner"
RseRelearner.open = false
+1 -1
View File
@@ -7,7 +7,7 @@ local Chrome = require("src.ui.game3.chrome")
local Kit = require("src.ui.game3.rse.scene_kit")
local Screens = require("src.ui.game3.screens")
local OptionMenu = {}
local OptionMenu = { isMenu = true }
OptionMenu.ID = "option"
OptionMenu.VISIBLE = 7
+1 -1
View File
@@ -6,7 +6,7 @@ local Kit = require("src.ui.game3.rse.scene_kit")
local Gfx = require("src.ui.game3.rse.pokeblock_gfx")
local Pokeblock = require("src.core.game3.rse.pokeblock")
local Case = {}
local Case = { isMenu = true }
Case.ID = "rse_pokeblock_case"
Case.open = false
+1 -1
View File
@@ -2358,7 +2358,7 @@ function Pokedex.draw(s)
love.graphics.pop()
end
local Host = {}
local Host = { isMenu = true }
Pokedex.Host = Host
Host._s = nil
Host._step = nil
+1 -1
View File
@@ -50,7 +50,7 @@ Pokenav.SCREENS = SCREENS
local Shell = {}
Shell.__index = Shell
local Host = {}
local Host = { isMenu = true }
Pokenav.Host = Host
-- pokeemerald/src/pokenav.c:388
+1 -1
View File
@@ -6,7 +6,7 @@ local RomText = require("src.core.game3.rom_text")
local ItemsData = require("src.core.game3.items_data")
local Gfx = require("src.ui.game3.rse.pokeblock_gfx")
local PBag = {}
local PBag = { isMenu = true }
PBag.ID = "rse_pyramid_bag"
PBag.open = false
+1 -1
View File
@@ -606,7 +606,7 @@ function RegionMap.draw(s)
love.graphics.pop()
end
local Host = {}
local Host = { isMenu = true }
RegionMap.Host = Host
Host._s = nil
Host._step = nil
+1 -1
View File
@@ -14,7 +14,7 @@ local RomText = require("src.core.game3.rom_text")
local Flags = require("src.core.game3.scripting.flags")
local Dex = require("src.core.game3.dex")
local SaveMenu = {}
local SaveMenu = { isMenu = true }
SaveMenu.open = false
SaveMenu.cursor = 1 -- 1=YES 2=NO
+1 -1
View File
@@ -9,7 +9,7 @@ local Bag = require("src.core.game3.bag")
local MoneyBox = require("src.ui.game3.money_box")
local RomText = require("src.core.game3.rom_text")
local ShopMenu = {}
local ShopMenu = { isMenu = true }
ShopMenu.open = false
ShopMenu.mode = "root"
+3
View File
@@ -24,6 +24,8 @@ end
-- opts.drawUnder: if true, still draw layers below this one.
function Stack.push(id, mod, opts)
opts = opts or {}
local isMenu = opts.isMenu
if isMenu == nil then isMenu = mod and mod.isMenu end
if opts.hideBelow ~= false then
local Message = package.loaded["src.ui.game3.message"]
if Message and Message.closeStay then Message.closeStay() end
@@ -37,6 +39,7 @@ function Stack.push(id, mod, opts)
Stack._layers[#Stack._layers + 1] = {
id = id,
mod = mod,
isMenu = isMenu == true,
drawUnder = opts.drawUnder and true or false,
hideBelow = opts.hideBelow ~= false, -- default hide layers underneath
fullscreen = opts.fullscreen,
+1 -1
View File
@@ -10,7 +10,7 @@ local RomText = require("src.core.game3.rom_text")
local ModRuntime = require("src.mods.Runtime")
local FrlgData = require("src.ui.game3.start_menu_frlg")
local StartMenu = {}
local StartMenu = { isMenu = true }
local function se(id)
pcall(function() require("src.core.game3.audio").playSe(require("src.core.game3.se_ids").resolve(id)) end)
+1 -1
View File
@@ -17,7 +17,7 @@ local Strings = require("src.core.Strings")
local RomText = require("src.core.game3.rom_text")
local ItemsData = require("src.core.game3.items_data")
local SummaryMenu = {}
local SummaryMenu = { isMenu = true }
-- pokefirered/src/pokemon_summary_screen.c:2139
function SummaryMenu.heldItemText(mon)
+1 -1
View File
@@ -18,7 +18,7 @@ local SummaryData = require("src.core.game3.summary_data")
local SummaryChrome = require("src.ui.game3.summary_chrome")
local RomText = require("src.core.game3.rom_text")
local TmCase = {}
local TmCase = { isMenu = true }
TmCase.open = false
TmCase.cursor = 1
+1 -1
View File
@@ -4,7 +4,7 @@ local Stack = require("src.ui.game3.stack")
local FrlgFont = require("src.ui.game3.frlg_font")
local RomText = require("src.core.game3.rom_text")
local TrainerCard = {}
local TrainerCard = { isMenu = true }
TrainerCard.open = false
TrainerCard._session = nil
+11 -1
View File
@@ -232,6 +232,15 @@ local ROWS = {
if speed == 1 then return Strings("NORMAL") end
return Strings("%dX", speed)
end },
{ label = Strings.source("ORIENTATION"), key = "orientation", port = true,
cycle = function(options, delta)
local Orientation = require("src.core.Orientation")
options.orientation = Orientation.cycle(options.orientation, delta)
Orientation.apply(options.orientation)
end,
text = function(options)
return Strings(require("src.core.Orientation").modeLabel(options.orientation))
end },
{ label = Strings.source("ZOOM"), key = "zoom", port = true,
cycle = function(options, delta, game)
local Zoom = require("src.render.Zoom")
@@ -500,7 +509,7 @@ local GROUPS = {
{ id = "group.speed", label = Strings.source("SPEED"),
members = { "textSpeed", "speed" } },
{ id = "group.video", label = Strings.source("VIDEO"),
members = { "videoMode", "faithfulRes", "screenPos", "fpsCap", "vsync",
members = { "videoMode", "orientation", "faithfulRes", "screenPos", "fpsCap", "vsync",
"logicClock" } },
{ id = "group.graphics", label = Strings.source("GRAPHICS"),
members = { "color", "uiLetterbox", "shaderfx", "shaderfx2", "frame" } },
@@ -586,6 +595,7 @@ local function buildRows()
-- The descriptor and the save key stay, so a build that grows a printer
-- only has to drop this test.
local hidden = row.key == "print"
or (row.key == "orientation" and osName ~= "Android" and osName ~= "iOS")
or (isNX and row.key == "videoMode")
or (not showTouch and (row.id == "touchControls"
or row.id == "touchLayout" or row.id == "haptics"
+5 -2
View File
@@ -3121,8 +3121,10 @@ function OverworldState:billsHousePokemonList()
end
table.insert(items, { label = Strings("CANCEL") })
-- TextBoxBorder b=10,c=9 at (0,0) -> total tw=11, th=12
Game.stack:push(Menu.new(Game, items,
{ tx = 0, ty = 0, tw = 11, th = 12 }))
local menu = Menu.new(Game, items,
{ tx = 0, ty = 0, tw = 11, th = 12 })
menu.isMenu = true
Game.stack:push(menu)
end
Game.stack:push(TextBox.new(Game, t._BillsHousePokemonListText1
or Strings("BILL's favorite\nPOKéMON list!"), openList))
@@ -3693,6 +3695,7 @@ function OverworldState:openPC(onDone)
menu = Menu.new(Game, items,
{ tx = 0, ty = 0, tw = 16, th = #items * 2 + 2, onCancel = logOff,
noSound = true })
menu.isMenu = true
-- engine/menus/pc.asm:5
Game.stack:push(TextBox.new(Game,
(Game.data.text or {})._TurnedOnPC1Text
+2 -1
View File
@@ -2703,7 +2703,8 @@ function World:mapMusicSong(mapId)
local def = self.maps and self.maps[mapId]
-- ENGINE_ROCKETS_IN_MAHOGANY / _RADIO_TOWER (data/events/engine_flags.asm:40,:36)
return World.mapMusicLabel(audio, def and def.music,
self:engineFlag(22), self:engineFlag(18))
self:engineFlag(self:engineFlagId("ENGINE_ROCKETS_IN_MAHOGANY", 22)),
self:engineFlag(self:engineFlagId("ENGINE_ROCKETS_IN_RADIO_TOWER", 18)))
end
function World:playMapMusic()
@@ -0,0 +1,98 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/BSA/10-02-26-00-userreported/shots/2617"
return function(game)
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
print("RESULT route110_surf_bridge failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
end
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
if not check(game.phase == "boot" and game.boot, "boot_ready") then return finish() end
game:_handleBootAction({ action = "new_game", name = "NICK", gender = 0 })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local Player = require("src.core.game3.player")
local Collision = require("src.core.game3.collision")
local Field = require("src.core.game3.field")
local Space = require("src.core.game3.scripting.space")
if not check(Runtime.getSession() ~= nil, "session_ready") then return finish() end
require("src.core.game3.encounters").onStep = function() return nil end
require("src.core.game3.trainer_sight").check = function() return false end
Map.load(Runtime._mod, game, "EM_ROUTE110", { x = 15, y = 29, facing = "down" })
U.wait(90)
if Space.vm and Space.vm.stop then Space.vm:stop() end
Field.unlock()
Player.surfing, Player.dismounting, Player.surfHopping = true, false, false
Player.elevation, Player.currentElevation = 1, 1
Player.facing, Player.turnArmed, Player.turnTimer = "down", true, 0
if not check(Map.current == "EM_ROUTE110" and Player.cellX == 15 and Player.cellY == 29
and Collision.isWater(15, 29), "normal_water_setup") then return finish() end
local function state(label)
print(string.format("STATE %s x=%d y=%d surfing=%s dismounting=%s elevation=%s current=%s behavior=%s coll=%s moving=%s locked=%s",
label, Player.cellX, Player.cellY, tostring(Player.surfing), tostring(Player.dismounting),
tostring(Player.elevation), tostring(Player.currentElevation),
tostring(Collision.behavior(Player.cellX, Player.cellY)), tostring(Collision.cell(Player.cellX, Player.cellY)),
tostring(Player.moving), tostring(Field.locked)))
end
local function step(dir)
local x, y = Player.cellX, Player.cellY
for _ = 1, 24 do
U.hold(game, dir, 1)
if Player.moving then break end
end
for _ = 1, 80 do
if not Player.moving then break end
U.wait(1)
end
state(dir)
return x ~= Player.cellX or y ~= Player.cellY
end
local Popup = require("src.ui.game3.map_name_popup")
for _ = 1, 360 do
if not Popup.isActive() then break end
U.wait(1)
end
if not check(not Popup.isActive(), "map_popup_finished") then return finish() end
if not check(step("up") and Map.current == "EM_ROUTE110" and Player.cellX == 15
and Player.cellY == 28 and Player.surfing and not Player.dismounting
and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, 28),
"before_bridge_clear_water_by_input") then return finish() end
state("before_bridge_clear_water")
check(U.still(game, DIR .. "/2617_before_bridge_on_water.png"), "shot_before")
if not check(step("down") and Map.current == "EM_ROUTE110" and Player.cellX == 15
and Player.cellY == 29 and Player.surfing and not Player.dismounting
and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, 29),
"returned_to_causal_setup_by_input") then return finish() end
state("before_bridge")
if not check(step("down") and Player.cellY == 30, "entered_bridge_by_input") then return finish() end
if not check(Player.surfing and not Player.dismounting and Player.elevation == 1,
"retain_surf_under_bridge") then return finish() end
check(U.still(game, DIR .. "/2617_under_bridge_after_entry.png"), "shot_under_bridge")
check(step("down") and Player.cellY == 31 and Player.surfing, "bridge_second_row")
check(step("down") and Player.cellY == 32 and Player.surfing, "bridge_third_row")
if not check(step("down") and Map.current == "EM_ROUTE110" and Player.cellX == 15
and Player.cellY == 33 and Player.surfing and not Player.dismounting
and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, 33),
"exit_bridge_to_south_water") then return finish() end
for _, y in ipairs({ 34, 35 }) do
if not check(step("down") and Map.current == "EM_ROUTE110" and Player.cellX == 15
and Player.cellY == y and Player.surfing and not Player.dismounting
and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, y),
"south_water_row" .. y .. "_retains_surf") then return finish() end
end
state("south_water_clear_of_bridge")
check(U.still(game, DIR .. "/2617_south_water_clear_of_bridge.png"), "shot_exit")
finish()
end
+11 -1
View File
@@ -65,7 +65,17 @@ return function(game)
end
local function phases(name)
U.wait(90)
local pairs = { "general__rom_082d4b54", "general__rom_082d4b6c" }
local pairs = {}
for index, mapId in ipairs({ "FR_ROUTE_19", "FR_ROUTE_20" }) do
local def = game.data and game.data.maps and game.data.maps[mapId]
local layout = def and Map.ensureMidLayout(game, mapId, def)
local pair = layout and layout.pair
if type(pair) ~= "string" or pair == "" or pair == pairs[1] then
result(false, name .. "_distinct_loaded_pair_" .. mapId)
return false
end
pairs[index] = pair
end
for index, phase in ipairs({ 2, 4 }) do
local target = phase * 16 + 3
local ready = false
@@ -0,0 +1,70 @@
local U = require("tests.drivers.util")
local Music = require("src.core.Music")
local FlagNames = require("src.core.gen2.FlagNames")
return function(game)
U.wait(60)
local world = game.world
local failures = 0
local function check(ok, label)
if not ok then failures = failures + 1 end
print((ok and "PASS " or "FAIL ") .. label)
end
if not (world and world.map) then
print("FAIL 2623 gen2 world did not boot")
return love.event.quit(1)
end
check(game.mods and game.mods.safeMode == true, "2623 mods disabled before boot")
world.checkTrainerBattle = function() return false end
local tower = world:engineFlagId("ENGINE_ROCKETS_IN_RADIO_TOWER", 18)
local mahogany = world:engineFlagId("ENGINE_ROCKETS_IN_MAHOGANY", 22)
check(type(tower) == "number" and type(mahogany) == "number",
"2623 Rocket engine flags resolve")
if not (tower and mahogany) then return love.event.quit(1) end
world.events:set(FlagNames.events.EVENT_RADIO_TOWER_ROCKET_TAKEOVER, false)
world.events:set(FlagNames.events.EVENT_RADIO_TOWER_CIVILIANS_AFTER, true)
world.mapScenes.RADIO_TOWER_5F = 2
world.mapScenes.MAHOGANY_MART_1F = 0
local dir = os.getenv("POKEPORT_SHOT_DIR")
local function enter(mapId, x, y)
if not world:warpToMapId(mapId, x, y, "up") then
check(false, "2623 load " .. mapId)
return false
end
U.wait(120)
check(world.map.id == mapId, "2623 entered " .. mapId)
return world.map.id == mapId
end
world:setEngineFlag(tower, true)
if not enter("GOLDENROD_CITY", 5, 10) then return love.event.quit(1) end
check(Music.current() == "Music_GoldenrodCity", "2623 ordinary Goldenrod music")
for floor = 1, 5 do
local mapId = "RADIO_TOWER_" .. floor .. "F"
world:setEngineFlag(tower, true)
if not enter(mapId, 2, 6) then return love.event.quit(1) end
print("2623 " .. mapId .. " flag=" .. tower
.. " active=" .. tostring(world:engineFlag(tower))
.. " resolved=" .. tostring(world:mapMusicSong(mapId))
.. " playing=" .. tostring(Music.current()))
check(world:engineFlag(tower), "2623 takeover active " .. mapId)
check(Music.current() == "Music_RocketTheme", "2623 rocket_music_floor_" .. floor)
if dir then U.still(game, dir .. "/2623_radio_tower_" .. floor .. "f_takeover.png") end
world:setEngineFlag(tower, false)
world:playMapMusic()
U.wait(120)
check(Music.current() == "Music_GoldenrodCity", "2623 cleared_music_floor_" .. floor)
end
world:setEngineFlag(mahogany, true)
if not enter("MAHOGANY_MART_1F", 2, 6) then return love.event.quit(1) end
check(world:engineFlag(mahogany), "2623 Mahogany Rockets active")
check(Music.current() == "Music_RocketHideout", "2623 mahogany_rocket_music")
if dir then U.still(game, dir .. "/2623_mahogany_mart_rocket_music.png") end
world:setEngineFlag(mahogany, false)
world:playMapMusic()
U.wait(120)
check(Music.current() == "Music_CherrygroveCity", "2623 mahogany_cleared_music")
print("RESULT 2623 failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
end
+151
View File
@@ -0,0 +1,151 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/menu_speed_bug2578"
local failed = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failed = failed + 1 end
return ok
end
local function gen1(game)
game.speedOverride = 200
game.save.party = { require("src.pokemon.Pokemon").new(game.data, "PIKACHU", 20) }
U.teleport(game, "PALLET_TOWN", 10, 12, "down")
local Screens = require("src.ui.Screens")
local Game = require("src.core.Game")
local opts = game.save.options
opts.speedOverworld, opts.speedBattle, opts.speedMenu = 4, 10, 2
game.speedOverride = nil
check(game:logicSpeed() == 4, "2578_gen1_field_speed")
for _, name in ipairs({ "StartMenu", "PartyMenu", "BagMenu" }) do
Screens.push(game, name)
U.wait(20)
check(Game.speedCategoryInStack(game.stack) == "menu" and game:logicSpeed() == 2,
"2578_gen1_field_" .. name .. "_menu_speed")
check(U.still(game, DIR .. "/2578_gen1_field_" .. name .. ".png"), "2578_gen1_" .. name .. "_shot")
game.stack:pop()
check(game:logicSpeed() == 4, "2578_gen1_" .. name .. "_pop_restores_field")
end
local Menu = require("src.ui.Menu")
local function reachPCMenu()
for _ = 1, 600 do
local top = game.stack:top()
if getmetatable(top) == Menu then return top end
U.tap(game, "a")
U.wait(2)
end
error("actual interactive PC menu was not reached")
end
game.overworld:openPC()
local pc = reachPCMenu()
check(#pc.items >= 3 and pc.noSound and Game.speedCategoryInStack(game.stack) == "menu"
and game:logicSpeed() == 2, "2578_gen1_pc_main_uses_MENU_SPEED")
check(U.still(game, DIR .. "/2578_gen1_pc_main.png"), "2578_gen1_pc_main_shot")
U.tap(game, "b")
U.wait(2)
check(game.stack:top() == game.overworld and game:logicSpeed() == 4,
"2578_gen1_pc_main_pop_restores_field")
game.speedOverride = 200
U.teleport(game, "BILLS_HOUSE", 2, 3, "up")
game.speedOverride = nil
game.overworld:billsHousePokemonList()
local bill = reachPCMenu()
check(#bill.items == 5 and bill.items[1].keepOpen and Game.speedCategoryInStack(game.stack) == "menu"
and game:logicSpeed() == 2, "2578_gen1_bill_pc_viewer_uses_MENU_SPEED")
check(U.still(game, DIR .. "/2578_gen1_bill_pc_viewer.png"), "2578_gen1_bill_pc_viewer_shot")
U.tap(game, "b")
U.wait(2)
check(game.stack:top() == game.overworld and game:logicSpeed() == 4,
"2578_gen1_bill_pc_viewer_pop_restores_field")
game.speedOverride = 200
U.teleport(game, "PALLET_TOWN", 10, 12, "down")
game.speedOverride = nil
local battle = require("src.battle.BattleState").newWild(game, "RATTATA", 3)
game.overworld:pushBattle(battle)
game.speedOverride = 200
for _ = 1, 1500 do
if game.stack:top() == battle and battle.phase == "menu" then break end
U.tap(game, "a")
U.wait(2)
end
game.speedOverride = nil
check(Game.speedCategoryInStack(game.stack) == "battle" and game:logicSpeed() == 10,
"2578_gen1_actual_battle_speed")
for _, name in ipairs({ "PartyMenu", "BagMenu" }) do
Screens.push(game, name, { battle = battle })
U.wait(20)
check(game:logicSpeed() == 2, "2578_gen1_battle_" .. name .. "_menu_speed")
check(U.still(game, DIR .. "/2578_gen1_battle_" .. name .. ".png"), "2578_gen1_battle_" .. name .. "_shot")
game.stack:pop()
check(game:logicSpeed() == 10, "2578_gen1_battle_" .. name .. "_pop_restores_battle")
end
end
local function gen3(game)
game.speedOverride = 200
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED", gender = 0 })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local session = Runtime.getSession()
assert(session, "new game did not reach field")
session.party = {}
require("src.core.game3.party").giveMon(session, 25, 20)
local Stack = require("src.ui.game3.stack")
local Start = require("src.ui.game3.start_menu")
local Party = require("src.ui.game3.party_menu")
local Bag = require("src.ui.game3.bag_menu")
game.options.speedOverworld, game.options.speedBattle, game.options.speedMenu = 4, 10, 2
game.speedOverride = nil
check(game:logicSpeed() == 4, "2578_gen3_field_speed")
local menus = {
{ id = "start", open = function() Start.show({ session = session, game = game }) end, close = Start.close },
{ id = "party", open = function() Party.show(session.party, nil, { session = session }) end, close = Party.close },
{ id = "bag", open = function() Bag.show(session, { session = session }) end, close = Bag.close },
}
for _, menu in ipairs(menus) do
menu.open()
U.wait(120)
check(Stack.has(menu.id) and game:logicSpeed() == 2, "2578_gen3_field_" .. menu.id .. "_menu_speed")
check(U.still(game, DIR .. "/2578_gen3_field_" .. menu.id .. ".png"), "2578_gen3_" .. menu.id .. "_shot")
menu.close()
check(game:logicSpeed() == 4, "2578_gen3_" .. menu.id .. "_pop_restores_field")
end
local Battle = require("src.core.game3.battle")
local Bridge = require("src.core.game3.battle_bridge")
local started, err = Bridge.startWild(Runtime._mod, game, { species = 16, level = 3 }, { fade = false })
assert(started, tostring(err))
game.speedOverride = 200
for _ = 1, 1800 do
if Battle._phase == "command" then break end
U.tap(game, "a")
U.wait(2)
end
game.speedOverride = nil
check(Battle.isActive() and game:logicSpeed() == 10, "2578_gen3_actual_battle_speed")
Party.show(session.party, nil, { session = session, battle = true, mode = "battle_switch" })
U.wait(120)
check(game:logicSpeed() == 2, "2578_gen3_battle_party_menu_speed")
check(U.still(game, DIR .. "/2578_gen3_battle_party.png"), "2578_gen3_battle_party_shot")
Party.close()
check(game:logicSpeed() == 10, "2578_gen3_battle_party_pop_restores_battle")
Bag.show(session, { session = session, battle = true })
U.wait(120)
check(game:logicSpeed() == 2 and Stack.top().fullscreen == false, "2578_gen3_nonfullscreen_battle_bag_menu_speed")
check(U.still(game, DIR .. "/2578_gen3_battle_bag.png"), "2578_gen3_battle_bag_shot")
Bag.close()
check(game:logicSpeed() == 10, "2578_gen3_battle_bag_pop_restores_battle")
end
return function(game)
local ok, err = xpcall(function()
if game.phase ~= nil then gen3(game) else gen1(game) end
end, debug.traceback)
if not ok then failed = failed + 1; print("FAIL 2578_driver " .. tostring(err)) end
print((failed == 0 and "PASS " or "FAIL ") .. "2578_menu_speed failures=" .. failed)
love.event.quit(failed == 0 and 0 or 1)
end
@@ -70,12 +70,15 @@ do
eq(stale.options.speedOverworld, 7, "a stale Runtime game is untouched")
check(wrote() > 0, "the change is persisted")
g.phase = "field"
eq(g:logicSpeed(), 2, "logicSpeed follows the menu change")
eq(g:speedCategory(), "menu", "the actual OPTION layer owns its speed while open")
eq(g:logicSpeed(), 1, "open OPTION keeps MENU SPEED after changing OVERWORLD SPEED")
check(selectRow("textSpeed"), "TEXT SPEED row reachable")
OptionMenu.handleInput(fakeInput("right"))
eq(Options.block(g.options).textSpeed, 2, "TEXT SPEED lands on the game's cart block")
OptionMenu.close()
eq(g:speedCategory(), "overworld", "closing OPTION restores the field category")
eq(g:logicSpeed(), 2, "the changed OVERWORLD SPEED applies after OPTION closes")
menu.state = "options"
menu:frame({ new = {}, held = {} })
eq(state.textSpeed, 2, "closing OPTION refreshes the boot text speed for NEW GAME")
+7 -6
View File
@@ -11,8 +11,9 @@ local Game3 = require("src.core.Game3")
local battleActive = false
package.loaded["src.core.game3.battle"] = { isActive = function() return battleActive end }
local stackBusy = false
package.loaded["src.ui.game3.stack"] = { busy = function() return stackBusy end, clear = function() end }
local Stack = require("src.ui.game3.stack")
local StartMenu = require("src.ui.game3.start_menu")
Stack.clear()
local function newGame3(phase)
local g = setmetatable({
@@ -29,13 +30,13 @@ Input:applyBindings(nil)
do
local g = newGame3("field")
stackBusy = true
eq(g:speedCategory(), "overworld", "menu over the field inherits overworld")
Stack.push("start", StartMenu)
eq(g:speedCategory(), "menu", "an explicit field menu owns MENU SPEED")
g:keypressed("1")
eq(g.options.speedOverworld, 2, "1 with a field menu open bumps OVERWORLD SPEED")
eq(g.options.speedOverworld, 2, "1 with a field menu open syncs OVERWORLD SPEED")
eq(g.options.speedMenu, 2, "1 with a field menu open syncs MENU SPEED")
eq(g.options.speedBattle, 2, "and syncs BATTLE SPEED")
stackBusy = false
Stack.pop("start")
eq(g:logicSpeed(), 2, "walk speed sticks after the menu closes")
battleActive = true
eq(g:speedCategory(), "battle", "battle stays battle")
@@ -0,0 +1,146 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
local GameVersion = require("src.core.GameVersion")
local MB = require("src.core.game3.mb")
local Collision = require("src.core.game3.collision")
local InteractionScripts = require("src.core.game3.scripting.interaction_scripts")
local session = { version = "emerald", map = "SURF_BRIDGE_TEST" }
local stops = 0
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
package.loaded["src.core.game3.audio"] = { stopSurfMusic = function() stops = stops + 1 end }
package.loaded["src.core.game3.bike"] = { rse = function() return nil end }
package.loaded["src.core.game3.field_effects"] = {
leaveTallGrass = function() end,
tallGrassAt = function() end,
}
package.loaded["src.core.game3.field"] = { locked = false }
package.loaded["src.core.game3.objects"] = { hasMap = function() return false end }
local Player = require("src.core.game3.player")
local game = { save = { position = {} } }
local function setup(version, cells, surfing, elevation)
GameVersion.set(version)
session.version = version
local layout = { width = #cells, height = 1, pair = "surf_bridge_test" }
function layout:collAt(x) return cells[x + 1][1] end
function layout:elevAt(x) return cells[x + 1][2] end
function layout:midAt(x) return x end
function layout:collArray()
local coll = {}
for i, cell in ipairs(cells) do coll[i] = cell[1] end
return coll
end
local behaviors = {}
for i, cell in ipairs(cells) do behaviors[i - 1] = cell[3] or MB.NORMAL end
InteractionScripts.behaviors[layout.pair] = behaviors
Collision.bindMap(game, session.map, { midLayout = layout, pair = layout.pair, warps = {} })
game.world = nil
Player.reset(0, 0, "right")
Player.surfing = surfing == true
Player.underwater = false
Player.elevation = elevation or cells[1][2]
Player.currentElevation = Player.elevation
stops = 0
end
local function settle()
Player._onStepDone = function() end
for _ = 1, 40 do
if not Player.moving then break end
Player.tick(game)
end
check(not Player.moving, "step completes")
end
local function step(label)
eq(Player.tryMove("right", game, false), "step", label .. " accepted")
settle()
end
local water = { 0x29, 1, MB.OCEAN_WATER }
local bridge15 = { 0, 15, MB.BRIDGE_OVER_OCEAN }
local bridge1 = { 0, 1, MB.BRIDGE_OVER_OCEAN }
local shore = { 0, 3, MB.NORMAL }
-- pokeemerald/src/field_player_avatar.c:693
setup("emerald", { water, bridge15, bridge1, water, shore }, true, 1)
step("water to multilevel bridge")
check(Player.surfing and not Player.dismounting, "retain_surf_under_bridge")
eq(Player.currentElevation, 1, "multilevel bridge keeps lower layer")
check(not Collision.isWater(1, 0), "bridge collision stays land for upper deck")
step("multilevel bridge to lower bridge")
check(Player.surfing, "retain_surf_on_elevation1_bridge")
step("bridge to water")
check(Player.surfing and Player.cellX == 3, "exit_bridge_to_water")
eq(stops, 0, "bridge traversal keeps Surf music")
eq(Player.tryMove("right", game, false), "step", "elevation3 shore accepted")
check(Player.surfing and Player.dismounting and Player.jumping, "shore starts Surf dismount hop")
eq(stops, 1, "shore stops Surf music once")
settle()
check(not Player.surfing and not Player.dismounting, "shore finishes on foot")
eq(Player.currentElevation, 3, "shore reaches default elevation")
setup("emerald", { { 0, 4 }, bridge15, { 0, 4 } }, false, 4)
step("upper deck to multilevel bridge")
check(not Player.surfing and not Player.jumping, "upper deck remains walking")
eq(Player.currentElevation, 4, "upper deck keeps upper layer")
step("multilevel bridge to upper deck")
check(not Player.surfing, "upper deck never enables Surf")
setup("emerald", { water, { 0x07, 15, MB.BRIDGE_OVER_OCEAN } }, true, 1)
eq(Player.tryMove("right", game, false), "blocked", "bridge support blocks Surf")
check(Player.surfing and not Player.moving and Player.cellX == 0, "support bump preserves Surf")
setup("emerald", { water, { 0, 4 } }, true, 1)
local result, why = Player.tryMove("right", game, false)
eq(result, "blocked", "wrong elevation land rejects dismount")
eq(why, "elevation", "wrong elevation collision reason")
check(Player.surfing, "wrong elevation bump preserves Surf")
setup("emerald", { water, shore }, true, 1)
game.world = { npcs = { { cellX = 1, cellY = 0, passable = false } } }
result, why = Player.tryMove("right", game, false)
eq(result, "blocked", "occupied shore rejects dismount")
eq(why, "entity", "occupied shore collision reason")
eq(stops, 0, "occupied shore keeps Surf music")
-- pokeemerald/src/field_player_avatar.c:443
setup("emerald", { water, bridge15 }, true, 1)
check(Player.forcedStep("right", 8), "forced bridge step accepted")
check(not Player.dismounting and not Player.jumping, "forced bridge step keeps Surf movement")
eq(Player.stepFrames, 8, "forced bridge retains requested speed")
settle()
check(Player.surfing, "forced_step_retains_surf_under_bridge")
eq(stops, 0, "forced bridge keeps Surf music")
setup("emerald", { water, shore }, true, 1)
check(Player.forcedStep("right", 8), "forced shore step accepted")
check(Player.dismounting and Player.jumping, "forced shore starts dismount")
eq(Player.stepFrames, 16, "forced shore retains dismount timing")
settle()
check(not Player.surfing, "forced shore finishes on foot")
setup("emerald", { water, bridge15 }, true, 1)
check(Player.forceStep("right"), "step without dismount setup accepted")
check(not Player.dismounting, "forceStep has no dismount setup")
settle()
check(Player.surfing, "finish_step_retains_surf_without_dismount")
for _, version in ipairs({ "firered", "leafgreen" }) do
-- pokefirered/src/field_player_avatar.c:568
setup(version, { water, { 0, 1, MB.PUDDLE }, { 0, 1, MB.SHALLOW_WATER }, water, shore }, true, 1)
for i = 1, 3 do
step(version .. " shallow step " .. i)
check(Player.surfing and not Player.dismounting, version .. " shallow water retains Surf")
end
step(version .. " shore")
check(not Player.surfing and Player.currentElevation == 3, version .. " shore dismount preserved")
eq(stops, 1, version .. " shore stops Surf music once")
end
GameVersion.set("firered")
T.finish("game3_surf_bridge_bug2617_test")
+9 -4
View File
@@ -22,7 +22,8 @@ local function stack(...) return { states = { ... } } end
local battle = { isBattle = true }
local overworld = { isOverworld = true }
local overlay = {} -- a party menu/choice box/naming screen/text box: no marker
local overlay = {}
local menu = { isMenu = true }
eq(Game.speedCategoryInStack(nil), "menu", "a nil stack falls to menu")
eq(Game.speedCategoryInStack(stack()), "menu", "an empty stack falls to menu")
@@ -35,16 +36,20 @@ eq(Game.speedCategoryInStack(stack(battle)), "battle",
"a battle alone resolves to battle")
eq(Game.speedCategoryInStack(stack(overworld, overlay)), "overworld",
"a menu opened while walking inherits overworld")
"unmarked dialogue while walking inherits overworld")
eq(Game.speedCategoryInStack(stack(battle, overlay)), "battle",
"a menu opened mid-battle inherits battle, not menu")
"unmarked battle dialogue inherits battle")
eq(Game.speedCategoryInStack(stack(overworld, overlay, overlay)), "overworld",
"the inheritance walk sees through more than one stacked overlay")
eq(Game.speedCategoryInStack(stack(overworld, battle)), "battle",
"a battle opened over the overworld reads as battle, not the overworld underneath it")
eq(Game.speedCategoryInStack(stack(overworld, battle, overlay)), "battle",
"and a menu on top of THAT still reads as battle")
"dialogue on top of battle still reads as battle")
eq(Game.speedCategoryInStack(stack(overworld, menu)), "menu",
"an explicit field menu owns MENU SPEED")
eq(Game.speedCategoryInStack(stack(overworld, battle, menu, overlay)), "menu",
"dialogue over an explicit battle menu inherits MENU SPEED")
-- ------- Game:_resolveLogicSpeed: category -> save.options key -> clamp
@@ -0,0 +1,102 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local SaveData = require("src.core.SaveData")
local Save = require("src.core.gen2.Save")
local Orientation = require("src.core.Orientation")
local Settings = require("src.import.LauncherSettings")
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
local Game2 = require("src.core.Game2")
local StateStack = require("src.core.StateStack")
local realLoad, realSave = SaveData.loadOptions, SaveData.saveOptions
local realApply = Orientation.apply
local applied
Orientation.apply = function(mode) applied = mode; return true end
local function memfs()
local files = {}
return {
write = function(path, content) files[path] = content; return true end,
read = function(path) return files[path] end,
remove = function(path) files[path] = nil; return true end,
getInfo = function(path) return files[path] and { type = "file" } end,
}
end
local function orientationRow(model)
for _, section in ipairs(model.sections) do
for _, row in ipairs(section.rows) do
if row.label == "ORIENTATION" then return row end
end
end
end
for _, osName in ipairs({ "Android", "iOS", "OS X" }) do
love.system.getOS = function() return osName end
for _, version in ipairs({ "red", "yellow", "gold", "silver", "crystal", "firered", "emerald" }) do
local fs = memfs()
local seed = SaveData.defaultOptions()
seed.orientation = "landscape"
seed.gold = { orientation = "portrait", textSpeed = "FAST" }
realSave(seed, fs)
SaveData.loadOptions = function() return realLoad(fs) end
SaveData.saveOptions = function(opts) return realSave(opts, fs) end
local model = Settings.open({}, version)
local row = orientationRow(model)
T.eq(row ~= nil, osName ~= "OS X", osName .. " " .. version .. " row visibility")
if row then
T.eq(row.value(), Orientation.modeLabel("landscape"), version .. " launcher reads flat orientation")
applied = nil
T.eq(row.step(1), true, version .. " orientation row steps")
T.eq(applied, "reverseLandscape", version .. " launcher applies selection live")
T.eq(model.opts.orientation, "reverseLandscape", version .. " launcher writes shared root")
T.eq(model.opts.gold.orientation, "portrait", version .. " launcher ignores stale local copy")
model.save()
T.eq(realLoad(fs).orientation, "reverseLandscape", version .. " selection persists flat")
T.eq(Save.loadOptions(fs).orientation, "reverseLandscape", version .. " shared value loads into Gen2")
end
SaveData.loadOptions, SaveData.saveOptions = realLoad, realSave
end
local options = Save.defaultOptions()
options.orientation = "landscape"
local stack = setmetatable({}, { __index = StateStack })
stack:init()
local game = { options = options, stack = stack }
local menu = OptionsMenu.new(game, { options = options })
stack:push(menu)
local row
for _, item in ipairs(menu.rows) do if item.id == "orientation" then row = item end end
T.eq(row ~= nil, osName ~= "OS X", osName .. " in-game orientation visibility")
if row then
local focused = menu:focusRow("orientation")
T.check(focused ~= nil and focused ~= menu, osName .. " orientation lives in VIDEO group")
T.eq(focused:row().id, "orientation", osName .. " group focuses orientation")
applied = nil
focused:cycle(row, 1)
T.eq(options.orientation, "reverseLandscape", osName .. " actual menu cycles shared options")
T.eq(applied, "reverseLandscape", osName .. " actual menu applies selection live")
end
end
SaveData.loadOptions, SaveData.saveOptions = realLoad, realSave
for _, value in ipairs({ "landscape", "reverseLandscape", "bogus" }) do
local fs = memfs()
local seed = SaveData.defaultOptions()
seed.orientation = value
seed.gold = { orientation = "portrait", textSpeed = "FAST" }
realSave(seed, fs)
local options = Save.loadOptions(fs)
T.eq(options.orientation, Orientation.normalize(value), "root " .. value .. " wins with normalization")
options.orientation = "portrait"
T.check(Save.saveOptions(options, fs), "Gen2 orientation persistence returns success")
local written = realLoad(fs)
T.eq(written.orientation, "portrait", "Gen2 writes host orientation flat")
T.eq(written.gold.orientation, nil, "Gen2 write removes stale local orientation")
T.eq(written.gold.textSpeed, "FAST", "Gen2 cartridge option remains local")
T.eq(Save.loadOptions(fs).orientation, "portrait", "Gen2 host orientation round-trips")
end
T.eq(Save.loadOptions(memfs()).orientation, "auto", "missing Gen2 orientation defaults to AUTO")
love.system.getOS = function() return "OS X" end
local game = setmetatable({ options = Save.defaultOptions() }, { __index = Game2 })
game.options.orientation = "reverseLandscape"
applied = nil
game:applyOptions()
T.eq(applied, "reverseLandscape", "Game2 applies the loaded host orientation")
Orientation.apply = realApply
T.finish("Gen2 orientation bug 2586")
@@ -63,6 +63,58 @@ eq(World.mapMusicLabel(audio, 72, true, true), nil,
eq(World.mapMusicLabel(audio, nil, true, true), nil,
"a missing music byte resolves to nothing")
local function newWorld(engineFlagOrder)
local maps = { MAHOGANY_MART_1F = { music = MUSIC_MAHOGANY_MART } }
for floor = 1, 5 do
maps["RADIO_TOWER_" .. floor .. "F"] = { music = RADIO_TOWER_SENTINEL }
end
return setmetatable({
constants = { engineFlagOrder = engineFlagOrder },
maps = maps,
game = { save = { engineFlags = {} }, data = { audio = audio } },
}, { __index = World })
end
-- pokecrystal/constants/engine_flags.asm:25
local crystalOrder = {
[19] = "ENGINE_SAFARI_ZONE",
[20] = "ENGINE_ROCKETS_IN_RADIO_TOWER",
[23] = "ENGINE_REACHED_GOLDENROD",
[24] = "ENGINE_ROCKETS_IN_MAHOGANY",
}
for _, version in ipairs({
{ name = "Gold", tower = 18, mahogany = 22 },
{ name = "Crystal", tower = 19, mahogany = 23, order = crystalOrder },
}) do
local world = newWorld(version.order)
world.game.save.engineFlags[version.tower] = true
world.game.save.engineFlags[version.mahogany] = true
for floor = 1, 5 do
local mapId = "RADIO_TOWER_" .. floor .. "F"
eq(world:mapMusicSong(mapId), "Music_RocketTheme",
version.name .. " takeover uses its own engine flag on " .. mapId)
end
eq(world:mapMusicSong("MAHOGANY_MART_1F"), "Music_RocketHideout",
version.name .. " Mahogany Rocket state uses its own engine flag")
world.game.save.engineFlags = {}
for floor = 1, 5 do
local mapId = "RADIO_TOWER_" .. floor .. "F"
eq(world:mapMusicSong(mapId), "Music_GoldenrodCity",
version.name .. " cleared takeover restores city music on " .. mapId)
end
eq(world:mapMusicSong("MAHOGANY_MART_1F"), "Music_CherrygroveCity",
version.name .. " cleared Mahogany restores Cherrygrove music")
end
local crystal = newWorld(crystalOrder)
crystal.game.save.engineFlags[18] = true
crystal.game.save.engineFlags[22] = true
eq(crystal:mapMusicSong("RADIO_TOWER_1F"), "Music_GoldenrodCity",
"Crystal Safari flag does not start Radio Tower takeover music")
eq(crystal:mapMusicSong("MAHOGANY_MART_1F"), "Music_CherrygroveCity",
"Crystal reached-Goldenrod flag does not start Mahogany Rocket music")
local data = { audio = {
songs = {
Music_RocketHideout = { file = "rocket_hideout.wav" },
+222
View File
@@ -0,0 +1,222 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local Game = require("src.core.Game")
local Game2 = require("src.core.Game2")
local Game3 = require("src.core.Game3")
local StateStack = require("src.core.StateStack")
local FixedStep = require("src.core.FixedStep")
local Stack3 = require("src.ui.game3.stack")
local battleActive = false
package.loaded["src.core.game3.battle"] = { isActive = function() return battleActive end }
local g = setmetatable({
save = { party = {}, player = { name = "RED" }, inventory = {},
flags = {}, pcItems = {}, pokedex = { seen = {}, owned = {} },
options = { speedOverworld = 4, speedBattle = 10, speedMenu = 2 } },
data = { pokemon = {}, items = {}, maps = {}, text = {}, field = {} },
input = { wasPressed = function() return false end },
}, { __index = Game })
g.stack = setmetatable({}, { __index = StateStack })
g.stack:init()
local overworld, battle = { isOverworld = true }, { isBattle = true }
g.stack:push(overworld)
g.data.pokemon.PIKACHU = { id = "PIKACHU", dex = 25 }
local constructors = {
StartMenu = {}, PartyMenu = {}, BagMenu = {}, OptionsMenu = {},
PokedexMenu = {}, TrainerCard = {}, BoxMenu = {}, PlayerPC = {},
LeaguePC = {}, NamingScreen = {}, FlyMenu = {}, TownMap = {},
DexEntryMenu = { "PIKACHU" }, SummaryMenu = { { species = "PIKACHU", moves = {} } },
PrizeCounter = { {} },
}
for name, args in pairs(constructors) do
local screen = require("src.ui." .. name).new(g, unpack(args))
T.eq(screen.isMenu, true, name .. " returned instance is a menu owner")
g.stack:push(screen)
T.eq(g:logicSpeed(), 2, name .. " uses MENU SPEED over the overworld")
g.stack:push({})
T.eq(g:logicSpeed(), 2, name .. " nested dialogue inherits MENU SPEED")
g.stack:pop()
g.stack:pop()
T.eq(g:logicSpeed(), 4, name .. " pop restores OVERWORLD SPEED")
end
local shop = require("src.ui.ShopMenu").new(g, {})
T.eq(shop.isMenu, true, "ShopMenu actual returned generic instance owns MENU SPEED")
T.eq(Game.speedCategoryInStack({ states = { overworld, shop, {} } }), "menu",
"shop greetings and nested item lists inherit the concrete menu owner")
local Menu = require("src.ui.Menu")
local OW = require("src.world.OverworldController")
local owGameIndex, owGame
for i = 1, 100 do
local name, value = debug.getupvalue(OW.openPC, i)
if not name then break end
if name == "Game" then owGameIndex, owGame = i, value; break end
end
assert(owGameIndex, "openPC Game binding absent")
debug.setupvalue(OW.openPC, owGameIndex, g)
local baseInput, pressed = g.input, nil
g.input = { wasPressed = function(_, key) return pressed == key end,
isDown = function() return false end }
local function tap(screen, key)
pressed = key
screen:update(1 / 60)
pressed = nil
end
local function advanceToMenu()
for _ = 1, 600 do
local top = g.stack:top()
if getmetatable(top) == Menu then return top end
tap(top, "a")
end
error("actual interactive PC menu was not reached")
end
OW:openPC()
T.eq(g:logicSpeed(), 4, "PC opening text retains OVERWORLD SPEED")
local pc = advanceToMenu()
T.eq(#pc.items >= 3 and pc.noSound, true, "actual main PC chooser is reached")
T.eq(pc.isMenu, true, "main PC chooser concrete instance owns MENU SPEED")
T.eq(Game.speedCategoryInStack(g.stack), "menu", "main PC chooser category is menu")
T.eq(g:logicSpeed(), 2, "actual main PC chooser uses MENU SPEED")
pc.index = 2
tap(pc, "a")
T.eq(g.stack:top().isTextBox, true, "main PC chooser opens actual player PC access text")
T.eq(g:logicSpeed(), 2, "player PC access text inherits MENU SPEED")
local playerPC = advanceToMenu()
T.eq(playerPC ~= pc and #playerPC.items == 4, true, "actual player PC submenu is reached")
T.eq(g:logicSpeed(), 2, "player PC submenu uses MENU SPEED")
tap(playerPC, "b")
T.eq(g.stack:top(), pc, "player PC back returns the same main chooser")
T.eq(g:logicSpeed(), 2, "returned main PC chooser retains MENU SPEED")
tap(pc, "b")
T.eq(g.stack:top(), overworld, "main PC cancel returns to field")
T.eq(g:logicSpeed(), 4, "main PC cancel restores OVERWORLD SPEED")
for i, id in ipairs({ "EEVEE", "FLAREON", "JOLTEON", "VAPOREON" }) do
g.data.pokemon[id] = { id = id, name = id, dex = 132 + i }
end
OW:billsHousePokemonList()
T.eq(g:logicSpeed(), 4, "Bill viewer opening text retains OVERWORLD SPEED")
local bill = advanceToMenu()
T.eq(#bill.items == 5 and bill.items[1].keepOpen, true, "actual Bill PC viewer list is reached")
T.eq(bill.isMenu, true, "Bill PC viewer concrete instance owns MENU SPEED")
T.eq(Game.speedCategoryInStack(g.stack), "menu", "Bill PC viewer category is menu")
T.eq(g:logicSpeed(), 2, "actual Bill PC viewer uses MENU SPEED")
tap(bill, "a")
T.eq(getmetatable(g.stack:top()), require("src.ui.DexEntryMenu"),
"Bill viewer selection opens the actual DexEntryMenu")
T.eq(g.save.pokedex.seen.EEVEE, true, "Bill viewer preserves the selected species seen flag")
T.eq(g:logicSpeed(), 2, "Bill viewer nested DexEntryMenu uses MENU SPEED")
for _ = 1, 600 do
if g.stack:top() == bill then break end
tap(g.stack:top(), "b")
end
T.eq(g.stack:top(), bill, "DexEntryMenu back returns the same Bill viewer")
T.eq(g:logicSpeed(), 2, "returned Bill PC viewer retains MENU SPEED")
tap(bill, "b")
T.eq(g.stack:top(), overworld, "Bill viewer cancel returns to field")
T.eq(g:logicSpeed(), 4, "Bill viewer cancel restores OVERWORLD SPEED")
g.input = baseInput
debug.setupvalue(OW.openPC, owGameIndex, owGame)
local generic = Menu.new(g, { { label = "CHOICE" } })
T.eq(generic.isMenu, nil, "generic Menu remains transparent")
g.stack:push(generic)
T.eq(g:logicSpeed(), 4, "unowned generic choice retains OVERWORLD SPEED")
g.stack:pop()
for _, name in ipairs({ "PartyMenu", "BagMenu" }) do
g.stack:push(battle)
local screen = require("src.ui." .. name).new(g, { battle = { playerParty = {} } })
g.stack:push(screen)
T.eq(g:logicSpeed(), 2, "battle " .. name .. " uses MENU SPEED")
g.stack:pop()
T.eq(g:logicSpeed(), 10, "battle " .. name .. " pop restores BATTLE SPEED")
g.stack:push({})
T.eq(g:logicSpeed(), 10, "battle dialogue stays at BATTLE SPEED")
g.stack:pop()
g.stack:pop()
end
local party = require("src.ui.PartyMenu").new(g)
g.stack:push(party)
local function ticks(speed)
g.save.options.speedMenu = speed
party.blink = 0
FixedStep.clock = function() return 0 end
FixedStep.refreshPeriod = nil
FixedStep:init(function(dt) party:update(dt) end)
local multiplier = g:logicSpeed()
FixedStep.maxAccum = FixedStep.catchupLimit(multiplier, 1 / 60)
for _ = 1, 60 do FixedStep:update(1 / 60, multiplier) end
return party.blink
end
T.eq(ticks(1), 60, "real party advances 60 logic ticks per second at MENU SPEED 1")
T.eq(ticks(4), 240, "real party advances 240 logic ticks per second at MENU SPEED 4")
g.speedOverride = 20
T.eq(g:logicSpeed(), 20, "run override still wins over a menu")
g.linkSession = true
T.eq(g:logicSpeed(), 1, "link lock still wins over menu and override")
g.linkSession = nil
g.stack:push({ isFixedSpeed = true })
T.eq(g:logicSpeed(), 1, "minigame lock still wins over menu and override")
g.stack:pop()
g.stack:pop()
local g3 = setmetatable({ phase = "field",
options = { speedOverworld = 4, speedBattle = 10, speedMenu = 2 } }, { __index = Game3 })
Stack3.clear()
for _, name in ipairs({ "start_menu", "party_menu", "bag_menu", "option_menu", "pokedex",
"trainer_card", "summary_menu", "box_storage_ui", "pc_menu", "item_pc",
"hall_of_fame_pc", "naming", "region_map", "tm_case", "berry_pouch", "save_menu",
"fame_checker", "controls_menu", "mod_manager", "rse.option_menu", "rse.item_storage",
"rse.mailbox", "shop_menu", "daycare_menu", "prize_corner", "move_relearner", "easy_chat",
"rse.move_relearner", "rse.pokeblock_case", "rse.berry_tag", "rse.frontier_pass", "rse.pyramid_bag" }) do
local mod = require("src.ui.game3." .. name)
Stack3.push(name, mod, { fullscreen = false })
T.eq(g3:speedCategory(), "menu", name .. " owns the actual non-fullscreen layer")
T.eq(g3:logicSpeed(), 2, name .. " uses MENU SPEED")
Stack3.push("dialogue", {}, { fullscreen = true })
T.eq(g3:logicSpeed(), 2, name .. " nested text inherits menu speed")
Stack3.pop("dialogue")
Stack3.pop(name)
T.eq(g3:logicSpeed(), 4, name .. " pop restores field speed")
end
for _, name in ipairs({ "pokedex", "region_map", "pokenav.init" }) do
local mod = require("src.ui.game3.rse." .. name).Host
Stack3.push(name, mod)
T.eq(g3:logicSpeed(), 2, "RSE " .. name .. " actual delegated Host owns MENU SPEED")
Stack3.pop(name)
end
for _, name in ipairs({ "party_menu", "bag_menu" }) do
battleActive = true
Stack3.push(name, require("src.ui.game3." .. name), { fullscreen = false })
T.eq(g3:logicSpeed(), 2, "Gen3 battle " .. name .. " uses MENU SPEED")
Stack3.pop(name)
T.eq(g3:logicSpeed(), 10, "Gen3 battle " .. name .. " pop restores BATTLE SPEED")
end
Stack3.push("battle_message", {}, { fullscreen = true })
T.eq(g3:logicSpeed(), 10, "Gen3 unmarked fullscreen battle text stays at BATTLE SPEED")
Stack3.clear()
battleActive = false
Stack3.push("scene", {}, { fullscreen = true })
T.eq(g3:logicSpeed(), 4, "Gen3 unmarked field scene stays at OVERWORLD SPEED")
Stack3.clear()
Stack3.push("party", require("src.ui.game3.party_menu"))
g3.speedOverride = 20
T.eq(g3:logicSpeed(), 20, "Gen3 run override wins over menu")
package.loaded["src.core.game3.link"] = { link = {} }
T.eq(g3:logicSpeed(), 1, "Gen3 link lock wins over menu and override")
package.loaded["src.core.game3.link"] = nil
package.loaded["src.core.game3.minigames.common"] = { isActive = function() return true end }
T.eq(g3:logicSpeed(), 1, "Gen3 minigame lock wins over menu and override")
package.loaded["src.core.game3.minigames.common"] = nil
Stack3.clear()
local g2 = setmetatable({ options = { speed = 4, speedOverworld = 1, speedMenu = 10 } }, { __index = Game2 })
T.eq(g2:logicSpeed(), 4, "Gen2 retains legacy GAME SPEED")
T.finish("menu speed bug 2578")
+12 -1
View File
@@ -366,7 +366,7 @@ local options = OptionsMenu.new(optionsGame, {
})
-- The cart's seven rows, then the port's: CONTROLS, audio, PERFORMANCE,
-- speed, display, SHADER FX + SHADER FX 2 (the second slot added alongside
check("thirty-five rows", #OptionsMenu.ROWS, 35)
check("thirty-six row descriptors", #OptionsMenu.ROWS, 36)
check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame")
check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls")
check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol")
@@ -378,6 +378,17 @@ local function hasRow(rows, key)
for _, row in ipairs(rows) do if row.key == key then return true end end
return false
end
check("ORIENTATION is a descriptor", hasRow(OptionsMenu.ROWS, "orientation"), true)
do
local oldSystem = love.system
for _, osName in ipairs({ "Linux", "Android", "iOS" }) do
love.system = { getOS = function() return osName end }
local platformOptions = OptionsMenu.new(newGame(Save.newGame()))
check(osName .. " orientation row visibility",
hasRow(platformOptions.rows, "orientation"), osName ~= "Linux")
end
love.system = oldSystem
end
check("PRINT is still a descriptor", hasRow(OptionsMenu.ROWS, "print"), true)
check("but never reaches the screen", hasRow(options.rows, "print"), false)
check("and the save keeps its value",