Merge pull request #2321 from Nexhas28/nexhas28-fix-firered-nickname-mon-icon-scale

This commit is contained in:
bryanthaboi
2026-09-19 08:37:39 -04:00
committed by GitHub
6 changed files with 290 additions and 9 deletions
+4 -1
View File
@@ -2100,7 +2100,10 @@ local function start_post_catch_flow(catchRes)
gender = gender,
personality = personality,
seed = ename,
title = "YOUR POKEMON'S NICKNAME?",
-- pret naming_screen.c:1712 DrawMonTextEntryBox: gSpeciesNames[mon]
-- + gText_PkmnsNickname. The hand-written "YOUR POKEMON'S NICKNAME?"
-- was 141px wide and spilled over the frame's right edge.
title = Naming.monTitle(Pokemon.name(sp)),
onDone = function(nick)
if nick and nick ~= "" and nick ~= ename then
if mon then mon.nickname = nick end
+1 -1
View File
@@ -261,7 +261,7 @@ Natives.ALLOW = {
end)
local sname = (Pokemon.name and Pokemon.name(species)) or "POKéMON"
adapters.openNaming({
title = sname .. "'s nickname?",
title = require("src.ui.game3.naming").monTitle(sname),
template = "NICKNAME",
maxLen = 10, -- pret POKEMON_NAME_LENGTH
species = species,
+29 -6
View File
@@ -25,6 +25,15 @@ Naming.TEMPLATE = {
NICKNAME = "NICKNAME",
}
-- pret gText_PkmnsNickname ("'s nickname?"), prepended with gSpeciesNames[mon]
-- by DrawMonTextEntryBox — pokefirered/src/naming_screen.c:1712. Used by both
-- mon naming templates (CAUGHT_MON and NICKNAME).
function Naming.monTitle(speciesName)
local s = tostring(speciesName or "")
if s == "" then s = "POKéMON" end
return s .. "'s nickname?"
end
-- pret sKeyboardChars + sPageColumnXPos (cursor). Letters drawn via ROW_TEXT CLEARs.
local PAGES = {
{
@@ -90,9 +99,19 @@ local SIDE = { "PAGE", "BACK", "OK" }
-- Values below are on-screen top-left blit positions (px).
local L = {
titleX = 73, titleY = 33,
-- WIN_TEXT_ENTRY_BOX = {tilemapLeft 9, tilemapTop 4, width 16} → screen
-- x 72..200; the title prints at (1,1) inside it, so it has 127px before the
-- GBA's per-window clip (CopyGlyphToWindow) would truncate it.
titleMaxW = 127,
-- Player/rival icon CreateSprite(56,37); 16×32 → TL (48,21)
iconCX = 56, iconCY = 37,
iconW = 16, iconH = 32,
-- Mon icon CreateMonIcon(species, SpriteCallbackDummy, 56, 40) is a 32×32
-- sprite (Versions.MON_ICON_W/H) drawn unscaled, centred on the frame baked
-- into bg.png — pokefirered/src/naming_screen.c:1422. Reusing the 16×32
-- player box above would letterbox it to half size.
monIconCX = 56, monIconCY = 40,
monIconW = 32, monIconH = 32,
charY = 49,
-- Underscore CreateSprite(base+3,60) 8×8 → TL (base-1, 56)
underscoreBaseY = 56,
@@ -310,13 +329,15 @@ local function drawPlayerIcon(st)
if entry and entry.image then
local iw = entry.w or entry.image:getWidth()
local ih = entry.h or (entry.quads and entry.h) or entry.image:getHeight()
local sc = math.min(L.iconW / iw, L.iconH / ih)
local q = entry.quads and (entry.quads[1] or entry.quads[0])
local cx, cy = L.iconCX, 40 -- pokefirered/src/naming_screen.c:1422
-- A mon icon is 32×32 and fills its frame 1:1; the frontPic fallback
-- is 64×64 and shrinks into the same box.
local sc = math.min(L.monIconW / iw, L.monIconH / ih)
-- pret passes SpriteCallbackDummy, so the icon shows its frame 0.
local q = entry.quads and entry.quads[0]
if q then
love.graphics.draw(entry.image, q, cx, cy, 0, sc, sc, iw / 2, ih / 2)
love.graphics.draw(entry.image, q, L.monIconCX, L.monIconCY, 0, sc, sc, iw / 2, ih / 2)
else
love.graphics.draw(entry.image, cx, cy, 0, sc, sc, iw / 2, ih / 2)
love.graphics.draw(entry.image, L.monIconCX, L.monIconCY, 0, sc, sc, iw / 2, ih / 2)
end
return
end
@@ -705,7 +726,9 @@ function Naming.draw()
-- 5) Title + icon + typed name (above KB)
love.graphics.setColor(1, 1, 1, 1)
drawText(st.title, L.titleX, L.titleY)
-- Clamp to the text-entry window so an over-long title cannot spill over the
-- frame (pret blits glyphs into the window buffer and clips there).
drawText(st.title, L.titleX, L.titleY, { maxWidth = L.titleMaxW })
drawPlayerIcon(st)
@@ -0,0 +1,123 @@
-- #2319: the Gen 3 nickname screen drew the mon's 32x32 menu icon at half
-- size, because the mon branch reused the player OW sprite's 16x32 box.
-- pret pokefirered/src/naming_screen.c:1422 CreateMonIcon(species,
-- SpriteCallbackDummy, 56, 40, ...) draws it unscaled, showing frame 0.
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 function fakeImage(w, h)
return {
w = w, h = h,
getWidth = function() return w end,
getHeight = function() return h end,
getDimensions = function() return w, h end,
}
end
-- 32x32 icon frames stacked two high; 64x64 front pic fallback.
local ICON_IMG = fakeImage(32, 64)
local PIC_IMG = fakeImage(64, 64)
local QUAD0, QUAD1 = { frame = 0 }, { frame = 1 }
local monIcon, monFrontPic
package.loaded["src.core.game3.pokemon"] = {
icon = function() return monIcon end,
frontPic = function() return monFrontPic end,
}
package.loaded["src.ui.game3.stack"] = {
push = function() end, pop = function() end,
busy = function() return false end, drawOrder = function() return {} end,
}
local Versions = require("src.import.gba.versions")
local Naming = require("src.ui.game3.naming")
eq(Naming.L.monIconW, Versions.MON_ICON_W,
"mon box width matches the extracted icon width")
eq(Naming.L.monIconH, Versions.MON_ICON_H,
"mon box height matches the extracted icon height")
-- Normalise both draw signatures into one record:
-- draw(img, quad, x, y, rot, sx, sy, ox, oy) and draw(img, x, y, rot, sx, sy, ox, oy)
local draws = {}
local realDraw = love.graphics.draw
love.graphics.draw = function(a, b, c, d, e, f, g, h, i)
local rec
if type(b) == "table" then
rec = { img = a, quad = b, x = c, y = d, rot = e, sx = f, sy = g, ox = h, oy = i }
else
rec = { img = a, x = b, y = c, rot = d, sx = e, sy = f, ox = g, oy = h }
end
draws[#draws + 1] = rec
end
local function render(opts)
draws = {}
Naming.open(opts)
local ok, err = pcall(Naming.draw)
Naming.dismiss()
return draws, ok, err
end
local function drawsOf(ds, img)
local out = {}
for _, d in ipairs(ds) do
if d.img == img then out[#out + 1] = d end
end
return out
end
-- The mon icon: 1:1 on the (56, 40) frame centre, static frame 0.
monIcon = { image = ICON_IMG, w = 32, h = 32, sheetH = 64, frames = 2,
quads = { [0] = QUAD0, [1] = QUAD1 } }
monFrontPic = nil
local ds, ok, err = render({ template = "NICKNAME", species = 1, maxLen = 10 })
check(ok, "nickname draw runs headless: " .. tostring(err))
local icon = drawsOf(ds, ICON_IMG)
eq(#icon, 1, "the mon icon is drawn once")
local d = icon[1] or {}
eq(d.quad, QUAD0, "static frame 0, not the 1px-shifted frame 1")
eq(d.x, 56, "icon centre x = 56 (naming_screen.c:1422)")
eq(d.y, 40, "icon centre y = 40 (naming_screen.c:1422)")
eq(d.rot, 0, "no rotation")
eq(d.sx, 1, "32x32 icon drawn 1:1, not the player box's 0.5")
eq(d.sy, 1, "uniform scale")
eq(d.ox, 16, "origin x is the frame centre, so the draw covers the frame")
eq(d.oy, 16, "origin y is the frame centre")
-- frontPic fallback: 64x64 shrunk into the same 32x32 box (was 0.25).
monIcon = nil
monFrontPic = { image = PIC_IMG, w = 64, h = 64 }
local ds2 = render({ template = "CAUGHT_MON", species = 4 })
local pic = drawsOf(ds2, PIC_IMG)
eq(#pic, 1, "the front-pic fallback is drawn once")
eq(pic[1] and pic[1].x, 56, "fallback keeps the mon centre x")
eq(pic[1] and pic[1].y, 40, "fallback keeps the mon centre y")
eq(pic[1] and pic[1].sx, 0.5, "64x64 fallback fills the 32x32 mon box")
eq(pic[1] and pic[1].sy, 0.5, "uniform fallback scale")
eq(pic[1] and pic[1].ox, 32, "fallback origin is the pic centre")
-- Player/rival slot: still the 16x32 OW box.
monIcon, monFrontPic = nil, nil
local portrait = fakeImage(64, 64)
local ds3 = render({ template = "PLAYER", icon = portrait })
local p = drawsOf(ds3, portrait)
eq(#p, 1, "the player-slot portrait is drawn once")
eq(p[1] and p[1].x, 56, "player slot keeps its OW centre x")
eq(p[1] and p[1].y, 37, "player slot keeps its OW centre y")
eq(p[1] and p[1].sx, 0.25, "player OW box math is unchanged (16/64)")
eq(p[1] and p[1].sy, 0.25, "player OW box scale is uniform")
eq(p[1] and p[1].ox, 32, "player OW origin is the portrait centre")
-- Guard: the mon icon is never letterboxed below 1:1 again.
for _, dl in ipairs({ ds, ds2 }) do
for _, e in ipairs(drawsOf(dl, ICON_IMG)) do
check((e.sx or 0) >= 1, "mon icon scale is never below 1:1")
end
end
love.graphics.draw = realDraw
T.finish()
@@ -0,0 +1,132 @@
-- #2319 (second report): the nickname screen's title ran past the right edge of
-- the text box. The catch flow hand-wrote "YOUR POKEMON'S NICKNAME?" (141px in a
-- 127px window), where pret composes gSpeciesNames[mon] + gText_PkmnsNickname:
-- pokefirered/src/naming_screen.c:1712 DrawMonTextEntryBox
-- pokefirered/src/strings.c:772 gText_PkmnsNickname = "'s nickname?"
-- sWindowTemplates[WIN_TEXT_ENTRY_BOX] = {tilemapLeft 9, width 16} → screen
-- x 72..200, text printed at (1,1) → x 73, so the budget is 127px.
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")
-- --- exact GBA advance widths, from the extracted FRLG latin font ------------
local function loadTable(path, pattern)
local f = io.open(path, "r")
if not f then return nil end
local src = f:read("*a")
f:close()
local t = {}
for a, b in src:gmatch(pattern) do t[tonumber(a)] = tonumber(b) end
return t
end
local widths = loadTable("src/import/gba/chrome/fonts/latin_widths.lua", "%[(%d+)%]%s*=%s*(%d+)")
check(widths ~= nil and next(widths) ~= nil, "latin glyph widths are loadable")
-- TextIR.CHARMAP: glyph id -> character, inverted to character -> glyph id.
local charmap = require("src.core.game3.scripting.text_ir").CHARMAP
local glyphOf = {}
for gid, ch in pairs(charmap) do
if type(gid) == "number" and type(ch) == "string" and #ch == 1 then
glyphOf[ch] = gid
end
end
check(next(glyphOf) ~= nil, "TextIR.CHARMAP is invertible to character -> glyph id")
local function px(s)
local total = 0
for i = 1, #s do
local gid = glyphOf[s:sub(i, i)]
if gid then total = total + (widths[gid] or 0) end
end
return total
end
local LEFT, BUDGET = 73, 127 -- text-entry window starts at x=73, 127px of room
local function endsAt(s) return LEFT + px(s) end
-- --- the reported string genuinely overflowed -------------------------------
local reported = "YOUR POKEMON'S NICKNAME?"
check(px(reported) > BUDGET,
("the old hardcoded title (%dpx) really did overflow the %dpx window"):format(px(reported), BUDGET))
check(endsAt(reported) > 200,
"the old title ran past the window's right edge (x=200)")
-- --- Naming.monTitle mirrors pret -------------------------------------------
local Naming = require("src.ui.game3.naming")
eq(type(Naming.monTitle), "function", "Naming.monTitle exists")
eq(Naming.monTitle("CHARMANDER"), "CHARMANDER's nickname?",
"mon title is gSpeciesNames[mon] + gText_PkmnsNickname")
eq(Naming.monTitle("BULBASAUR"), "BULBASAUR's nickname?", "title tracks the species")
check(Naming.monTitle("CHARMANDER") ~= Naming.monTitle("SQUIRTLE"),
"title is species-derived, not a fixed caption")
eq(Naming.monTitle(nil), "POKéMON's nickname?", "a missing name falls back safely")
-- --- every real species title fits the window -------------------------------
-- Longest real species names (both 10 characters) plus the widest 10-character
-- name the font can produce: a strict upper bound over all caps names.
for _, name in ipairs({ "CHARMANDER", "FORRETRESS", "MASQUERAIN", "BUTTERFREE" }) do
local title = Naming.monTitle(name)
check(endsAt(title) <= 200,
("%q ends at x=%d, inside the box (<=200)"):format(title, endsAt(title)))
end
local widest = {}
for ch in pairs(glyphOf) do
if ch:match("^[A-Z]$") then widest[#widest + 1] = ch end
end
table.sort(widest, function(a, b)
local wa, wb = widths[glyphOf[a]] or 0, widths[glyphOf[b]] or 0
if wa == wb then return a < b end
return wa > wb
end)
local worstName = table.concat({ widest[1], widest[2], widest[3], widest[4], widest[5],
widest[6], widest[7], widest[8], widest[9], widest[10] })
local worstTitle = Naming.monTitle(worstName)
check(endsAt(worstTitle) <= 200,
("worst-case 10-char name %q ends at x=%d, still inside the box"):format(worstTitle, endsAt(worstTitle)))
-- --- the screen clamps the title to the window ------------------------------
-- pret blits glyphs into the window buffer (CopyGlyphToWindow), which clips at
-- the window edge; the port must pass that window as maxWidth rather than the
-- generic 240 default, which would let text run over the frame art.
eq(Naming.L.titleMaxW, BUDGET, "L.titleMaxW is the text-entry window's usable width")
eq(Naming.L.titleX, LEFT, "the title still starts at the window's left edge")
local FrlgFont = require("src.ui.game3.frlg_font")
local realDraw = FrlgFont.draw
local seen = {}
FrlgFont.draw = function(text, x, y, opts)
seen[#seen + 1] = { text = text, x = x, y = y, maxWidth = opts and opts.maxWidth }
return 0
end
package.loaded["src.core.game3.pokemon"] = {
icon = function() return nil end,
frontPic = function() return nil end,
}
package.loaded["src.ui.game3.stack"] = {
push = function() end, pop = function() end,
busy = function() return false end, drawOrder = function() return {} end,
}
local title = Naming.monTitle("BUTTERFREE")
Naming.open({ template = "CAUGHT_MON", species = 12, maxLen = 10, title = title })
local ok, err = pcall(Naming.draw)
Naming.dismiss()
FrlgFont.draw = realDraw
check(ok, "nickname draw runs headless: " .. tostring(err))
local drawn
for _, s in ipairs(seen) do
if s.text == title then drawn = s end
end
check(drawn ~= nil, "the title is drawn as its own run")
eq(drawn and drawn.x, LEFT, "title drawn at the window's left edge")
eq(drawn and drawn.y, 33, "title drawn at the template's y")
eq(drawn and drawn.maxWidth, BUDGET,
"title is clamped to the window, not the 240px default that overran the frame")
T.finish()
+1 -1
View File
@@ -124,7 +124,7 @@ for _, key in ipairs(expectedKeys) do
end
-- Check Naming screen layout coordinates match pokefirered
Naming.open({ template = "NICKNAME", species = 1, maxLen = 10, title = "YOUR POKEMON'S NICKNAME?" })
Naming.open({ template = "NICKNAME", species = 1, maxLen = 10, title = Naming.monTitle("BULBASAUR") })
check(Naming.isOpen(), "Naming screen is open")
-- Verify species icon positioning at (56, 40)