From def3f68f8a775b5df1955e6f243aa21b4fe4a41a Mon Sep 17 00:00:00 2001 From: Shane McGovern Date: Sat, 19 Sep 2026 12:51:48 +0100 Subject: [PATCH] Draw the Gen 3 nickname mon icon at its real 32x32 size The mon branch of drawPlayerIcon reused the player overworld sprite's 16x32 box to letterbox the icon, so a 32x32 Pokemon.icon came out at scale 0.5 -- half size inside the frame baked into bg.png. The 64x64 frontPic fallback was worse still, at 0.25. Give the mon its own box in the layout table (pret naming_screen.c:1422 CreateMonIcon(species, SpriteCallbackDummy, 56, 40) draws it unscaled on that centre), so the icon fills the frame 1:1 and the fallback shrinks to fit the same box. Also pin the frame to quads[0]: pret's dummy callback leaves the icon on its first frame, not the 1px-shifted second pose. The player/rival overworld slot keeps its 16x32 box untouched. Fixes #2319 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/ui/game3/naming.lua | 18 ++- .../nickname_mon_icon_scale_bug2319.lua | 123 ++++++++++++++++++ 2 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 tests/engine/nickname_mon_icon_scale_bug2319.lua diff --git a/src/ui/game3/naming.lua b/src/ui/game3/naming.lua index ba223177..4c11f955 100644 --- a/src/ui/game3/naming.lua +++ b/src/ui/game3/naming.lua @@ -93,6 +93,12 @@ local L = { -- 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 +316,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 diff --git a/tests/engine/nickname_mon_icon_scale_bug2319.lua b/tests/engine/nickname_mon_icon_scale_bug2319.lua new file mode 100644 index 00000000..bcdf503c --- /dev/null +++ b/tests/engine/nickname_mon_icon_scale_bug2319.lua @@ -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()