mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
275 lines
9.4 KiB
Lua
275 lines
9.4 KiB
Lua
-- Unit tests for Game 3 pause menu (StartMenu) exit flow and Main Menu exit option.
|
|
-- Run: luajit tests/game3_pause_and_main_menu_exit_test.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
local ROM_TEXT = { ["sStartMenuActionTable[3]"] = "{PLAYER}" }
|
|
local function romTextKey(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end
|
|
local function romTextPlain(key, ctx) return ((ROM_TEXT[key] or key):gsub("{PLAYER}", ctx and ctx.playerName or "")) end
|
|
package.loaded["src.core.game3.rom_text"] = {
|
|
plain = romTextPlain, box = romTextPlain, ascii = romTextPlain, has = function() return true end,
|
|
key = romTextKey, at = function(n, i, j, ctx) return romTextPlain(romTextKey(n, i, j), ctx) end,
|
|
count = function() return 0 end, list = function() return {} end,
|
|
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
|
|
}
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
local StartMenu = require("src.ui.game3.start_menu")
|
|
local Stack = require("src.ui.game3.stack")
|
|
local Boot = require("src.ui.game3.boot")
|
|
local Game3 = require("src.core.Game3")
|
|
|
|
-- Test 1: StartMenu EXIT flow and confirmation
|
|
do
|
|
local returnToTitleCalled = false
|
|
local dummyGame = {
|
|
returnToTitle = function()
|
|
returnToTitleCalled = true
|
|
end,
|
|
}
|
|
local session = { name = "RED", game = dummyGame }
|
|
|
|
StartMenu.resetCursor()
|
|
StartMenu.show({ session = session, game = dummyGame })
|
|
check(StartMenu.isOpen(), "StartMenu is open")
|
|
check(#StartMenu.ENTRIES > 0, "StartMenu has entries")
|
|
|
|
-- Locate EXIT entry index
|
|
local exitIdx = nil
|
|
for i, e in ipairs(StartMenu.ENTRIES) do
|
|
if e.id == "exit" then
|
|
exitIdx = i
|
|
break
|
|
end
|
|
end
|
|
check(exitIdx ~= nil, "StartMenu contains exit entry")
|
|
|
|
-- Move to EXIT entry
|
|
StartMenu.cursor = exitIdx
|
|
check(not StartMenu._confirmExit, "Not in confirm exit mode yet")
|
|
|
|
-- Press confirm on EXIT
|
|
StartMenu.confirm()
|
|
check(StartMenu._confirmExit, "Now in confirm exit mode")
|
|
eq(StartMenu._confirmCursor, 2, "Default confirm cursor is 2 (NO)")
|
|
|
|
-- Move toggles between 1 (YES) and 2 (NO)
|
|
StartMenu.move(1)
|
|
eq(StartMenu._confirmCursor, 1, "Cursor moved to 1 (YES)")
|
|
StartMenu.move(1)
|
|
eq(StartMenu._confirmCursor, 2, "Cursor moved back to 2 (NO)")
|
|
|
|
-- B / cancel cancels confirm mode without closing menu or quitting
|
|
StartMenu.cancel()
|
|
check(not StartMenu._confirmExit, "Cancel exited confirm mode")
|
|
check(StartMenu.isOpen(), "StartMenu is still open after canceling prompt")
|
|
check(not returnToTitleCalled, "returnToTitle not called on cancel")
|
|
|
|
-- Re-enter confirm mode and confirm NO
|
|
StartMenu.confirm()
|
|
check(StartMenu._confirmExit, "Re-entered confirm exit mode")
|
|
eq(StartMenu._confirmCursor, 2, "Defaulted to NO")
|
|
StartMenu.confirm()
|
|
check(not StartMenu._confirmExit, "Confirming NO exited confirm mode")
|
|
check(StartMenu.isOpen(), "StartMenu is still open after choosing NO")
|
|
check(not returnToTitleCalled, "returnToTitle not called on NO")
|
|
|
|
-- Re-enter confirm mode, switch to YES, and confirm
|
|
StartMenu.confirm()
|
|
check(StartMenu._confirmExit, "Re-entered confirm exit mode")
|
|
StartMenu.move(1)
|
|
eq(StartMenu._confirmCursor, 1, "Moved cursor to YES")
|
|
StartMenu.confirm()
|
|
check(not StartMenu._confirmExit, "Exited confirm mode on YES")
|
|
check(not StartMenu.isOpen(), "StartMenu closed on YES")
|
|
check(returnToTitleCalled, "returnToTitle was called on YES")
|
|
end
|
|
|
|
-- Test 2: Game3:returnToTitle cleans up and sets boot to title phase
|
|
do
|
|
local game = Game3.new()
|
|
game.session = { map = "FR_PALLET_TOWN", x = 5, y = 5 }
|
|
game.phase = "field"
|
|
|
|
game:returnToTitle()
|
|
eq(game.phase, "boot", "Game phase returned to boot")
|
|
check(game.session == nil, "Game session cleared")
|
|
check(game.boot ~= nil, "Game boot state created")
|
|
eq(game.boot.phase, Boot.PHASE.TITLE, "Boot phase is title")
|
|
eq(Stack.depth(), 0, "Stack is cleared")
|
|
end
|
|
|
|
-- Test 3: Boot main menu items and EXIT handling
|
|
do
|
|
local stateWithContinue = Boot.new()
|
|
Boot.setHasContinue(stateWithContinue, true)
|
|
local items1 = Boot.menuItems(stateWithContinue)
|
|
eq(#items1, 4, "Main menu has 4 items when continue save exists")
|
|
eq(items1[1], "CONTINUE", "Item 1 is CONTINUE")
|
|
eq(items1[2], "NEW GAME", "Item 2 is NEW GAME")
|
|
eq(items1[3], "MYSTERY GIFT", "Item 3 is MYSTERY GIFT")
|
|
eq(items1[4], "EXIT", "Item 4 is EXIT")
|
|
|
|
local stateNoContinue = Boot.new()
|
|
Boot.setHasContinue(stateNoContinue, false)
|
|
local items2 = Boot.menuItems(stateNoContinue)
|
|
eq(#items2, 2, "Main menu has 2 items when no continue save exists")
|
|
eq(items2[1], "NEW GAME", "Item 1 is NEW GAME")
|
|
eq(items2[2], "EXIT", "Item 2 is EXIT")
|
|
end
|
|
|
|
-- Test 4: Boot update choosing EXIT returns { action = "exit" }
|
|
do
|
|
local state = Boot.new()
|
|
Boot.setHasContinue(state, true)
|
|
state.phase = Boot.PHASE.MENU
|
|
state.menuIndex = 4 -- EXIT
|
|
state.fadeT, state.fadeTarget = 0, 0
|
|
|
|
local pressedKey = nil
|
|
local inputStub = {
|
|
wasPressed = function(self, key)
|
|
return key == pressedKey
|
|
end,
|
|
}
|
|
|
|
pressedKey = "a"
|
|
local act = Boot.update(state, inputStub, 1/60)
|
|
eq(act, nil, "First frame initiates fade")
|
|
eq(state.fadeThen, "exit", "Fade action set to exit")
|
|
|
|
-- Advance fade until finished
|
|
while state.fadeThen do
|
|
act = Boot.update(state, inputStub, 1/60)
|
|
end
|
|
check(type(act) == "table" and act.action == "exit", "Selecting EXIT returns { action = 'exit' }")
|
|
end
|
|
|
|
-- Test 5: Game3:_handleBootAction({ action = "exit" }) triggers returnToLauncher or onExit
|
|
do
|
|
local launcherCalled = false
|
|
local exitCalled = false
|
|
|
|
local game = Game3.new()
|
|
game.returnToLauncher = function()
|
|
launcherCalled = true
|
|
end
|
|
game.onExit = function()
|
|
exitCalled = true
|
|
end
|
|
|
|
game:_handleBootAction({ action = "exit" })
|
|
check(launcherCalled, "returnToLauncher called when available")
|
|
check(not exitCalled, "returnToLauncher prioritized over onExit")
|
|
|
|
launcherCalled = false
|
|
game.returnToLauncher = nil
|
|
game:_handleBootAction({ action = "exit" })
|
|
check(exitCalled, "onExit called when returnToLauncher is nil")
|
|
end
|
|
|
|
-- Test 6: Boot.draw handles main menu with and without continue without errors
|
|
do
|
|
local stateWith = Boot.new()
|
|
Boot.setHasContinue(stateWith, true)
|
|
stateWith.phase = Boot.PHASE.MENU
|
|
stateWith.continueInfo = {
|
|
name = "ASH",
|
|
hours = 12,
|
|
minutes = 34,
|
|
hasDex = true,
|
|
dexCount = 50,
|
|
badges = 4,
|
|
gender = 0,
|
|
}
|
|
local ListMenu = require("src.ui.game3.list_menu")
|
|
local realArrow = ListMenu.drawArrow
|
|
local arrows = {}
|
|
ListMenu.drawArrow = function(dir) arrows[#arrows + 1] = dir end
|
|
for idx = 1, 4 do
|
|
stateWith.menuIndex = idx
|
|
stateWith.menuScroll = idx == 4 and 4 or nil
|
|
arrows = {}
|
|
local ok, err = pcall(Boot.draw, stateWith)
|
|
check(ok, "drawMainMenu with continue at index " .. idx .. " runs without error: " .. tostring(err))
|
|
if idx == 4 then
|
|
eq(#arrows, 0, "no down arrow once EXIT is scrolled into view")
|
|
else
|
|
eq(arrows[1], "down", "down arrow flags EXIT below the fold at index " .. idx)
|
|
end
|
|
end
|
|
ListMenu.drawArrow = realArrow
|
|
|
|
local stateWithout = Boot.new()
|
|
Boot.setHasContinue(stateWithout, false)
|
|
stateWithout.phase = Boot.PHASE.MENU
|
|
for idx = 1, 2 do
|
|
stateWithout.menuIndex = idx
|
|
local ok, err = pcall(Boot.draw, stateWithout)
|
|
check(ok, "drawMainMenu without continue at index " .. idx .. " runs without error: " .. tostring(err))
|
|
end
|
|
end
|
|
|
|
-- Test 7: Hud.isMenuOpen reflects pause menu and other modal menus
|
|
do
|
|
local Hud = require("src.ui.game3.hud")
|
|
StartMenu.close()
|
|
check(not Hud.isMenuOpen(), "isMenuOpen is false when no menu open")
|
|
|
|
StartMenu.show({ session = {}, game = {} })
|
|
check(Hud.isMenuOpen(), "isMenuOpen is true when StartMenu open")
|
|
check(Hud.busy(), "Hud.busy is true when StartMenu open")
|
|
|
|
StartMenu.close()
|
|
check(not Hud.isMenuOpen(), "isMenuOpen is false after closing StartMenu")
|
|
end
|
|
|
|
-- Test 8: Runtime.update pauses field and pumpRtc while pause menu is open
|
|
do
|
|
local Runtime = require("src.core.game3.runtime")
|
|
local Field = require("src.core.game3.field")
|
|
local session = {
|
|
map = "FR_PALLET_TOWN",
|
|
x = 5, y = 5,
|
|
playtime = { hours = 1, minutes = 20, seconds = 30, vblanks = 0 },
|
|
}
|
|
local game = { session = session, save = {} }
|
|
Runtime.start(nil, game, session, { reason = "test", alreadyOnMap = true })
|
|
|
|
local fieldUpdateCount = 0
|
|
local origFieldUpdate = Field.update
|
|
Field.update = function(dt)
|
|
fieldUpdateCount = fieldUpdateCount + 1
|
|
end
|
|
|
|
-- 1. Normal field tick (unpaused)
|
|
Runtime.update(1 / 60)
|
|
eq(fieldUpdateCount, 1, "Field.update called when unpaused")
|
|
|
|
-- 2. Open pause menu (StartMenu)
|
|
StartMenu.show({ session = session, game = game })
|
|
local prevSeconds = session.playtime.seconds
|
|
local prevAcc = Runtime._playTimeAcc or 0
|
|
|
|
-- Tick many frames while pause menu is open (simulating fast-forward logic ticks)
|
|
for _ = 1, 120 do
|
|
Runtime.update(1 / 60)
|
|
end
|
|
|
|
eq(fieldUpdateCount, 1, "Field.update was NOT called while pause menu open")
|
|
eq(session.playtime.seconds, prevSeconds, "Playtime seconds did NOT tick while pause menu open")
|
|
eq(Runtime._playTimeAcc, prevAcc, "Playtime accumulator did NOT advance while pause menu open")
|
|
|
|
-- 3. Close pause menu
|
|
StartMenu.close()
|
|
Runtime.update(1 / 60)
|
|
eq(fieldUpdateCount, 2, "Field.update resumed after closing pause menu")
|
|
|
|
Field.update = origFieldUpdate
|
|
Runtime.stop(nil, game)
|
|
end
|
|
|
|
T.finish("game3_pause_and_main_menu_exit_test")
|