CLOSES #1474, CLOSES #1475, CLOSES #1477, CLOSES #1482, CLOSES #1512, CLOSES #1516, CLOSES #1558, CLOSES #1569, CLOSES #1578, CLOSES #1579

This commit is contained in:
bryanthaboi
2026-08-20 16:04:12 -04:00
parent 83463a5a59
commit 0f8f6d0e4f
51 changed files with 2331 additions and 205 deletions
@@ -0,0 +1,163 @@
-- Title -> main menu handoff (#1510) and the Oak-intro naming layout (#1511):
-- title.asm .finishedWaiting, oak_speech2.asm ChoosePlayerName
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Data = T.fixtures.load()
local SaveData = require("src.core.SaveData")
local Menu = require("src.ui.Menu")
local NamingScreen = require("src.ui.NamingScreen")
local OakSpeech = require("src.ui.OakSpeech")
local TitleState = require("src.ui.TitleState")
local function newStack()
local stack = { states = {} }
function stack:push(state, ...)
table.insert(self.states, state)
if type(state.enter) == "function" then state:enter(...) end
end
function stack:pop() return table.remove(self.states) end
function stack:top() return self.states[#self.states] end
return stack
end
local function newGame()
return {
data = Data,
save = SaveData.newGame(),
stack = newStack(),
input = { wasPressed = function() return false end,
isDown = function() return false end },
}
end
local function run(state, frames)
for _ = 1, frames do
if state.game.stack:top() ~= state then return end
state:update(1 / 60)
end
end
-- ------------------------------------------------- #1510: title -> menu
local game = newGame()
local title = TitleState.new(game, {})
game.stack:push(title)
title:toMenu()
local flash = game.stack:top()
T.check(flash ~= title, "START pushes a state before the menu")
T.eq(flash.isOpaque, true, "GBPalWhiteOutWithDelay3 covers the title art")
T.check(not title.menuOpen, "the title is still drawing itself mid-blink")
run(flash, 60)
local menu = game.stack:top()
T.check(getmetatable(menu) == Menu, "the blink hands off to the main menu")
T.eq(title.menuOpen, true, "MainMenu's ClearScreen takes the title art down")
T.eq(menu.tx, 0, "the CONTINUE / NEW GAME box is still at hlcoord 0,0")
-- OPTION returns to .mainMenuLoop, so its row must not close the box
local option
for _, item in ipairs(menu.items) do
if item.label == "OPTION" then option = item end
end
T.check(option ~= nil and option.keepOpen == true,
"OPTION keeps the main menu on the stack")
-- B: DisplayTitleScreen opens with GBPalWhiteOut
T.check(type(menu.onCancel) == "function", "the main menu cancels back out")
game.stack:pop()
menu.onCancel()
local back = game.stack:top()
T.eq(back.isOpaque, true, "backing out blinks white too")
T.eq(title.menuOpen, true, "the title art stays down until the blink ends")
run(back, 60)
T.eq(title.menuOpen, false, "and comes back once the blink is over")
T.eq(game.stack:top(), title, "leaving the menu lands back on the title")
-- ------------------------------------------- #1511: the intro NAME box
local ngame = newGame()
local naming = NamingScreen.new(ngame, {
presets = { "RED", "ASH", "JACK" }, introBox = true,
})
ngame.stack:push(naming)
local box = ngame.stack:top()
T.check(getmetatable(box) == Menu, "the preset list is a bordered menu")
-- DisplayIntroNameTextBox: TextBoxBorder at hlcoord 0,0 with b=$a, c=$9
T.eq(box.tx, 0, "the name box starts at column 0")
T.eq(box.ty, 0, "the name box starts at row 0")
T.eq(box.tw, 11, "c=$9 plus both border columns is 11 tiles wide")
T.eq(box.th, 12, "b=$a plus both border rows is 12 tiles tall")
T.eq(box.title, "NAME", "the NAME label rides the box's top border")
T.eq(box.itemY, 2, "wTopMenuItemY 2: the list is anchored down from the top")
T.eq(box.cancelable, false, "there is no way out of the naming choice")
-- every other NamingScreen caller keeps the old preset box
local plain = NamingScreen.new(newGame(), { presets = { "RED" } })
local pgame = plain.game
pgame.stack:push(plain)
T.eq(pgame.stack:top().tx, 4, "a non-intro preset list is unchanged")
T.eq(pgame.stack:top().title, nil, "and carries no header")
-- ------------------------------------------ #1511: the pic slide + box
local steps = OakSpeech.defaultSteps({})
local byId = {}
for _, step in ipairs(steps) do byId[step.id] = step end
-- oak_speech.asm:86-91 MovePicLeft, then a text_end box that stays up
T.eq(byId.ask_player_name.reveal, "wipe", "the player pic wipes in")
T.eq(byId.ask_player_name.stay, true, "and its question box stays on screen")
T.eq(byId.ask_rival_name.reveal, "fade", "the rival pic fades in")
T.eq(byId.ask_rival_name.stay, true, "and its box stays on screen too")
local sgame = newGame()
local speech = OakSpeech.new(sgame, function() end)
sgame.stack:push(speech)
speech.steps = steps
speech.step = 0
speech:runStep(byId.name_player)
local slide = sgame.stack:top()
T.check(slide ~= speech, "the name beat slides the pic before the box opens")
T.eq(speech.picSlide, 0, "the slide starts where the pic already sat")
-- six tiles, one per Delay3
run(slide, 6 * 3)
T.eq(speech.picSlide, 48, "OakSpeechSlidePicRight ends six tiles across")
T.check(sgame.stack:top() ~= slide, "the slide pops itself when it lands")
-- ------------------------------------- #1511: the question box stays up
local fgame = newGame()
local flow = OakSpeech.new(fgame, function() end)
fgame.stack:push(flow)
flow.steps = OakSpeech.defaultSteps(flow)
for i, step in ipairs(flow.steps) do
if step.id == "ask_player_name" then flow.step = i - 1 end
end
flow:advance()
local function pump(frames)
for _ = 1, frames do
local top = fgame.stack:top()
if top.update then top:update(1 / 60) end
if getmetatable(fgame.stack:top()) == Menu then return end
end
end
pump(400)
T.check(getmetatable(fgame.stack:top()) == Menu, "the preset list opens")
T.eq(fgame.stack.states[2], flow.holdBox,
"IntroducePlayerText's box is still on the stack under the name list")
T.check(flow.holdBox.done and flow.holdBox.stay,
"it is a `stay` box: text_end returns from PrintText without a wait")
local preset = fgame.stack:top()
preset.index = 2
fgame.input.wasPressed = function(_, b) return b == "a" end
preset:update(1 / 60)
fgame.input.wasPressed = function() return false end
T.eq(fgame.save.player.name, "RED", "picking a preset names the player")
T.eq(flow.holdBox, nil, "and takes the question box down with the list")
run(fgame.stack:top(), 6 * 3)
T.eq(flow.picSlide, 0, "OakSpeechSlidePicLeft puts the pic back")
T.finish("intro_title_naming_bug1510_1511")