Run the HoF dex rating through the standard text box (#314)

HoFDisplayPlayerStats prints its three dex texts (seen/owned, the
POKéDEX Rating: header, the tier line) through HoFPrintTextAndDelay ->
PrintText, the standard two-row bottom box, each followed by 120
DelayFrames.  The port hand-drew a 6-row box and flattened the tier
text's cont (\v) rows into plain newlines, so a 3+ row rating painted
its third row over the box's bottom border and dropped the rest.

Push the three texts as a chain of auto-advancing TextBox states
instead: the cont rows scroll inside the two-row box with the original's
A/B wait (_ContText -> ManualTextScroll), and each box closes itself
after the 120-frame hold.

Adds tests/parity_hof_rating.lua: box order/content and the auto-close
hold are asserted over a full headless induction.
This commit is contained in:
johnjohto
2026-07-28 07:32:35 -04:00
parent 72f83760b7
commit bfac5fe327
2 changed files with 147 additions and 54 deletions
+42 -54
View File
@@ -9,6 +9,7 @@
local Assets = require("src.render.Assets")
local Font = require("src.render.Font")
local TextBox = require("src.render.TextBox")
local Music = require("src.core.Music")
local Sound = require("src.core.Sound")
local TypeChart = require("src.battle.TypeChart")
@@ -64,17 +65,6 @@ local function dexRatingKey(owned)
return ("_DexRatingText_Own%dTo%d"):format(lo, lo + 9)
end
-- \n/\v/\f-marked extracted text, one Font.draw line at a time (same
-- technique as DexEntryMenu.lua's dex-description block)
local function drawTextBlock(text, x, y, maxY)
for line in (text:gsub("\v", "\n"):gsub("\f", "\n") .. "\n"):gmatch("(.-)\n") do
if maxY and y > maxY then break end
Font.draw(line, x, y)
y = y + 8
end
return y
end
function HallOfFame.new(game, onDone)
local self = setmetatable({}, HallOfFame)
self.game = game
@@ -182,26 +172,50 @@ function HallOfFame:update(dt)
self.phase = "player_stats"
self.timer = DEX_HOLD
elseif self.phase == "player_stats" then
-- name / play time / money boxes are up; then DexSeenOwnedText
-- name / play time / money boxes are up; then the dex texts
self.timer = self.timer - 1
if skip or self.timer <= 0 then
self.phase = "player_dex"
self.timer = DEX_HOLD
end
elseif self.phase == "player_dex" then
self.timer = self.timer - 1
if skip or self.timer <= 0 then
self.phase = "player_rating"
self.timer = DEX_HOLD
end
elseif self.phase == "player_rating" then
self.timer = self.timer - 1
if skip or self.timer <= 0 then
-- HoFFadeOutScreenAndMusic -> Credits lead-in (no A wait here)
self.game.stack:pop()
if self.onDone then self.onDone() end
self:showDexTexts()
end
end
-- player_dex / player_rating are driven by the TextBox chain that
-- showDexTexts pushes; nothing is timed here
end
-- HoFDisplayPlayerStats' three HoFPrintTextAndDelay calls: seen/owned,
-- "POKéDEX Rating:", then the tier text DisplayDexRating copied into
-- wDexRatingText -- each through PrintText (the standard two-row box, so
-- the tier text's \v rows scroll inside it) followed by 120 DelayFrames
-- with no button wait. Hand-drawing these flattened the \v scrolls and
-- painted the third row over the box's bottom border (#314).
function HallOfFame:showDexTexts()
local game = self.game
local text = game.data.text or {}
local seen, owned = self:dexSeenOwned()
local seenOwned = (text._DexSeenOwnedText
or Strings("POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}"))
:gsub("{NUM:wDexRatingNumMonsSeen[^}]*}", tostring(seen))
:gsub("{NUM:wDexRatingNumMonsOwned[^}]*}", tostring(owned))
local header = (text._DexRatingText or Strings("POKéDEX Rating{COLON}"))
:gsub("{COLON}", ":")
local rating = text[dexRatingKey(owned)] or Strings("Keep it up!")
local function finish()
-- HoFFadeOutScreenAndMusic -> Credits lead-in (no A wait here)
game.stack:pop()
if self.onDone then self.onDone() end
end
local function showRating()
game.stack:push(TextBox.new(game, rating, finish,
{ auto = { delay = DEX_HOLD } }))
end
local function showHeader()
self.phase = "player_rating"
game.stack:push(TextBox.new(game, header, showRating,
{ auto = { delay = DEX_HOLD } }))
end
game.stack:push(TextBox.new(game, seenOwned, showHeader,
{ auto = { delay = DEX_HOLD } }))
end
-- HoFDisplayMonInfo: TextBoxBorder (0,2) b=9,c=10 + LEVEL/TYPE labels
@@ -263,28 +277,6 @@ function HallOfFame:drawPlayerStats()
Font.draw(("¥%d"):format(save.money or 0), 4 * 8, 10 * 8)
end
function HallOfFame:drawDexBox(kind)
local save = self.game.save
local text = self.game.data.text or {}
Font.drawBox(0, 12, 20, 6)
love.graphics.setColor(0, 0, 0, 1)
if kind == "seen" then
local seen, owned = self:dexSeenOwned()
local seenOwned = text._DexSeenOwnedText
or Strings("POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}")
seenOwned = seenOwned
:gsub("{NUM:wDexRatingNumMonsSeen[^}]*}", tostring(seen))
:gsub("{NUM:wDexRatingNumMonsOwned[^}]*}", tostring(owned))
drawTextBlock(seenOwned, 1 * 8, 14 * 8, 17 * 8)
else
local _, owned = self:dexSeenOwned()
local ratingHeader = (text._DexRatingText or Strings("POKéDEX Rating{COLON}")):gsub("{COLON}", ":")
Font.draw(ratingHeader, 1 * 8, 14 * 8)
local rating = text[dexRatingKey(owned)] or Strings("Keep it up!")
drawTextBlock(rating, 1 * 8, 15 * 8, 17 * 8)
end
end
function HallOfFame:draw()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
@@ -309,14 +301,10 @@ function HallOfFame:draw()
elseif self.phase == "player_stats" then
self:drawPic(self.playerPic)
self:drawPlayerStats()
elseif self.phase == "player_dex" then
elseif self.phase == "player_dex" or self.phase == "player_rating" then
-- the TextBox chain draws the dex texts over the stat boxes
self:drawPic(self.playerPic)
self:drawPlayerStats()
self:drawDexBox("seen")
elseif self.phase == "player_rating" then
self:drawPic(self.playerPic)
self:drawPlayerStats()
self:drawDexBox("rating")
end
love.graphics.setColor(1, 1, 1, 1)
+105
View File
@@ -0,0 +1,105 @@
-- Parity test: the Hall of Fame dex rating runs through the standard
-- PrintText text box (#314).
-- Self-contained: run via `luajit tests/parity_hof_rating.lua`; also
-- dofile'd by tests/run_tests.lua's aggregator.
--
-- pokered's HoFDisplayPlayerStats (engine/movie/hall_of_fame.asm) prints
-- three texts back to back through HoFPrintTextAndDelay -> PrintText, each
-- followed by 120 DelayFrames: _DexSeenOwnedText, _DexRatingText
-- ("POKéDEX Rating:"), then the tier line copied into wDexRatingText by
-- DisplayDexRating (engine/events/pokedex_rating.asm). PrintText is the
-- standard two-row bottom box, so the tier text's `cont` (\v) rows scroll
-- inside it -- each with the original's ▼ + A/B wait (_ContText ->
-- ManualTextScroll), even mid-ceremony.
--
-- The port instead hand-drew a 6-row box and flattened \v into plain
-- newlines with drawTextBlock, so a 3+ row rating ("You finally got at /
-- least 50 species! / Be sure to get ...") painted its third row on top of
-- the bottom border and dropped the rest (#314).
--
-- The invariant: after the stat boxes, the induction pushes TextBox states
-- for the seen/owned counts, the "POKéDEX Rating:" header, and the tier
-- text, in that order, and each closes itself after the 120-frame hold.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local Data = require("src.core.Data")
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
local Font = require("src.render.Font")
if not pcall(Font.encode, "A") then Font.load(Data) end
local S = require("tests.harness").suite("parity HoF dex rating")
local check, eq = S.check, S.eq
local SaveData = require("src.core.SaveData")
local TextBox = require("src.render.TextBox")
local HallOfFame = require("src.ui.HallOfFame")
local pressed = {}
local fakeInput = { wasPressed = function(_, b) return pressed[b] or false end,
isDown = function() return false end }
local stack = { states = {} }
function stack:push(s, ...) table.insert(self.states, s) if s.enter then s:enter(...) end end
function stack:pop() local s = table.remove(self.states) if s and s.exit then s:exit() end return s end
function stack:top() return self.states[#self.states] end
local game = { data = Data, input = fakeInput, stack = stack,
save = SaveData.newGame() }
game.save.party = { { species = "PIKACHU", level = 81, hp = 100,
stats = { hp = 100 },
moves = { { id = "THUNDERBOLT", pp = 35 } } } }
-- 55 owned lands in the Own50To59 tier from the issue's screenshot
local seen, owned = {}, {}
for i = 1, 55 do seen[i] = true; owned[i] = true end
game.save.pokedex = { seen = seen, owned = owned }
-- record every TextBox the induction pushes, with its full text
local shown = {}
local realNew = TextBox.new
TextBox.new = function(g, text, onDone, opts)
local self = realNew(g, text, onDone, opts)
local lines = {}
for _, page in ipairs(self.pages) do
for _, line in ipairs(page) do lines[#lines + 1] = line end
end
shown[#shown + 1] = { text = table.concat(lines, " "), opts = opts }
return self
end
local done = false
stack:push(HallOfFame.new(game, function() done = true end))
-- drive the induction with A held: mashing skips the mon phases; the dex
-- texts must still run their course on their own
pressed.a = true
local guard = 0
while not done and guard < 30000 do
guard = guard + 1
local top = stack:top()
if top and top.update then top:update(1 / 60) end
end
pressed.a = nil
TextBox.new = realNew -- restore: run_tests dofiles all parity suites in one process
check(done, "induction runs to completion")
eq(#shown, 3, "three text boxes follow the stat boxes (seen/owned, header, rating)")
check(shown[1] ~= nil and shown[1].text:find("Seen", 1, true) ~= nil
and shown[1].text:find("55", 1, true) ~= nil,
"first box is the seen/owned count, got: " ..
tostring(shown[1] and shown[1].text))
check(shown[2] ~= nil and shown[2].text:find("Rating", 1, true) ~= nil,
"second box is the POKéDEX Rating: header, got: " ..
tostring(shown[2] and shown[2].text))
check(shown[3] ~= nil and shown[3].text:find("least 50 species!", 1, true) ~= nil,
"third box is the Own50To59 tier text, got: " ..
tostring(shown[3] and shown[3].text))
-- HoFPrintTextAndDelay = PrintText + 120 DelayFrames: each box pops itself
-- after the hold (the rating's `cont` rows still wait for A/B, exactly like
-- pokered's _ContText -> ManualTextScroll -- the A held above walks them)
for i, box in ipairs(shown) do
check(box.opts and box.opts.auto ~= nil,
("box %d auto-closes after its 120-frame hold"):format(i))
end
S.finish()