Files
gen1recomp/tests/engine/menu_speed_bug2578_test.lua
T
2026-10-02 13:52:21 -04:00

223 lines
9.8 KiB
Lua

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")