diff --git a/src/core/game3/pokemon.lua b/src/core/game3/pokemon.lua index 555cdd34..a0b30cb6 100644 --- a/src/core/game3/pokemon.lua +++ b/src/core/game3/pokemon.lua @@ -1160,8 +1160,18 @@ function Pokemon.replaceMove(mon, slot, newMoveId) return old end +-- An egg reads as the language's own EGG whatever its nickname holds: pret's +-- GetMonData(MON_DATA_NICKNAME) returns gText_EggNickname for any egg +-- (pokefirered/src/pokemon.c:3020). The stored nickname is only a placeholder +-- -- the cart's daycare writes タマゴ (daycare.c:1100), this engine "EGG". +local function eggName(mon) + if Pokemon.isEgg(mon) then return Strings("EGG") end +end + function Pokemon.displayMonName(mon) if not mon then return "POKéMON" end + local egg = eggName(mon) + if egg then return egg end local nick = mon.nickname if type(nick) == "string" and nick ~= "" then return nick end if mon.name and mon.name ~= "" then return mon.name end @@ -1246,6 +1256,13 @@ function Pokemon.isEgg(mon) return (mon.isEgg == true) or (mon.egg == true) or (mon.species == 412) end +-- pokefirered/src/pokemon.c:3245 MON_DATA_SPECIES_OR_EGG: an egg's menu icon is +-- SPECIES_EGG's, not the species it will hatch into (party_menu.c:2655). +function Pokemon.speciesOrEgg(mon) + if Pokemon.isEgg(mon) then return Pokemon.SPECIES_EGG end + return Pokemon.speciesOf(mon) +end + local function read_rgba(species) local cache = resolve_cache(Pokemon._cache) local root = (Extract.CACHE_ROOT or "data/generated/gba") .. "/pokemon" @@ -1572,6 +1589,8 @@ end function Pokemon.displayName(mon) if not mon then return "?????" end + local egg = eggName(mon) + if egg then return egg end if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end -- Prefer pack name over host species string when we can resolve. local sp = Pokemon.speciesOf(mon) diff --git a/src/core/game3/scripting/natives.lua b/src/core/game3/scripting/natives.lua index 2fe06362..ebaaa2b5 100644 --- a/src/core/game3/scripting/natives.lua +++ b/src/core/game3/scripting/natives.lua @@ -79,10 +79,13 @@ local function boxedMon() (tonumber(session.monBoxId) or 0) + 1, (tonumber(session.monBoxPos) or 0) + 1) end +-- GetMonData(MON_DATA_NICKNAME) is gText_EggNickname for an egg +-- (pokefirered/src/pokemon.c:3020) local function nicknameOf(mon) if not mon then return "" end - if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end local Pokemon = require("src.core.game3.pokemon") + if Pokemon.isEgg(mon) then return Strings("EGG") end + if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end pcall(function() if not Pokemon._names then Pokemon.install(nil) end end) diff --git a/src/core/game3/scripting/natives_queries.lua b/src/core/game3/scripting/natives_queries.lua index 02b36acb..e1ec57a5 100644 --- a/src/core/game3/scripting/natives_queries.lua +++ b/src/core/game3/scripting/natives_queries.lua @@ -118,11 +118,13 @@ local function speciesFromNational(nat) return (ok and tonumber(sp)) or nat end --- pokefirered/src/field_specials.c:1671 +-- pokefirered/src/field_specials.c:1671; GetMonData(MON_DATA_NICKNAME) is +-- gText_EggNickname for an egg (pokemon.c:3020) local function nicknameOf(mon) if not mon then return "" end - if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end local Pokemon = require("src.core.game3.pokemon") + if Pokemon.isEgg(mon) then return Strings("EGG") end + if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end pcall(function() if not Pokemon._names then Pokemon.install(nil) end end) diff --git a/src/core/game3/scripting/natives_trade.lua b/src/core/game3/scripting/natives_trade.lua index 6c426b7f..7b6ba06c 100644 --- a/src/core/game3/scripting/natives_trade.lua +++ b/src/core/game3/scripting/natives_trade.lua @@ -137,8 +137,11 @@ local function speciesName(species) return (Pokemon.name and Pokemon.name(species)) or "" end +-- GetMonData(MON_DATA_NICKNAME) is gText_EggNickname for an egg +-- (pokefirered/src/pokemon.c:3020) local function nicknameOf(mon) if not mon then return "" end + if require("src.core.game3.pokemon").isEgg(mon) then return Strings("EGG") end if mon.nickname and mon.nickname ~= "" then return tostring(mon.nickname) end return speciesName(speciesOf(mon)) end diff --git a/src/core/game3/trade_scene.lua b/src/core/game3/trade_scene.lua index 4688520f..be5318fc 100644 --- a/src/core/game3/trade_scene.lua +++ b/src/core/game3/trade_scene.lua @@ -212,8 +212,11 @@ local function cryFinished(s) return done and true or false end +-- pokefirered/src/trade_scene.c:1239 GetMonData(MON_DATA_NICKNAME), which is +-- gText_EggNickname for an egg (pokemon.c:3020) local function nameOf(mon) if not mon then return "" end + if isEgg(mon) then return Strings("EGG") end local nick = mon.nickname or mon.name if type(nick) == "string" and nick ~= "" then return nick end local ok, Pokemon = pcall(require, "src.core.game3.pokemon") diff --git a/src/import/CacheContract.lua b/src/import/CacheContract.lua index 3e1c7d5a..3b2f5453 100644 --- a/src/import/CacheContract.lua +++ b/src/import/CacheContract.lua @@ -377,6 +377,8 @@ CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = { "data/generated/gba/pokemon/egg/hatch.rgba", "data/generated/gba/pokemon/egg/shard.rgba", "data/generated/gba/pokemon/front/412.rgba", + -- src/party_menu.c:2655 + "data/generated/gba/pokemon/icons/412.rgba", -- src/battle_records.c:563 "data/generated/gba/trainer_tower/manifest.lua", "data/generated/gba/trainer_tower/records_bg.rgba", diff --git a/src/import/gba/egg_extract.lua b/src/import/gba/egg_extract.lua index 4aee2c4a..917f111c 100644 --- a/src/import/gba/egg_extract.lua +++ b/src/import/gba/egg_extract.lua @@ -3,6 +3,7 @@ local Versions = require("src.import.gba.versions") local Lz77 = require("src.import.gba.lz77") local BgBake = require("src.import.gba.bg_bake") +local PokemonExtract = require("src.import.gba.pokemon_extract") local EggExtract = {} @@ -77,6 +78,10 @@ function EggExtract.run(rom, cache, opts) cache:write(cacheRoot .. "/pokemon/front/" .. EggExtract.SPECIES_EGG .. ".rgba", BgBake.bakeSpriteRgba(tiles, picBank, 0, PIC_W, PIC_H, false, false)) + -- src/party_menu.c:2655 draws an egg's icon from MON_DATA_SPECIES_OR_EGG + cache:write(cacheRoot .. "/pokemon/icons/" .. EggExtract.SPECIES_EGG .. ".rgba", + PokemonExtract.iconRgba(rom, EggExtract.SPECIES_EGG)) + cache:write(root .. "/manifest.lua", string.format([[ return { format_version = %d, @@ -102,7 +107,12 @@ function EggExtract.ready(cache, cacheRoot) for _, rel in ipairs({ "hatch.rgba", "shard.rgba", "manifest.lua" }) do if not cache:exists(root .. "/" .. rel) then return false end end - return cache:exists(cacheRoot .. "/pokemon/front/" .. EggExtract.SPECIES_EGG .. ".rgba") + for _, sub in ipairs({ "front", "icons" }) do + if not cache:exists(cacheRoot .. "/pokemon/" .. sub .. "/" .. EggExtract.SPECIES_EGG .. ".rgba") then + return false + end + end + return true end return EggExtract diff --git a/src/import/gba/pokemon_extract.lua b/src/import/gba/pokemon_extract.lua index 46b02ffb..f4beff83 100644 --- a/src/import/gba/pokemon_extract.lua +++ b/src/import/gba/pokemon_extract.lua @@ -648,9 +648,26 @@ local function extract_egg_moves(rom, num) return eggMoves end +-- gMonIconTable / gMonIconPaletteIndices entry for one species, both frames +-- (32x64 RGBA); they run past NUM_SPECIES, so SPECIES_EGG has its own icon. +local function icon_rgba(rom, sp, pals) + pals = pals or load_icon_pals(rom) + local w = Versions.MON_ICON_W or 32 + local iconH = (Versions.MON_ICON_H or 32) * 2 -- 64 (2 frames) + local iconBytes = Versions.MON_ICON_BYTES or math.floor(w * iconH / 2) -- 1024 for 32x64 + local off = gba_off(rom:u32(Versions.MON_ICON_TABLE + sp * 4)) + if not off then return string.rep(string.char(0, 0, 0, 0), w * iconH * 4) end + local palIdx = rom:get(Versions.MON_ICON_PAL_INDICES + sp) or 0 + if palIdx >= Versions.MON_ICON_PAL_COUNT then palIdx = 0 end + local pixels = decode_4bpp(rom:readBytes(off, iconBytes), w, iconH) + return bake_icon_rgba(pixels, pals[palIdx] or pals[0], w, iconH) +end + -- Exposed for tests (tests/engine/game3_egg_moves.lua). PokemonExtract.eggMovesFromRom = extract_egg_moves PokemonExtract.writeEggMovesLua = write_egg_moves_lua +-- src/import/gba/egg_extract.lua bakes the SPECIES_EGG icon with it. +PokemonExtract.iconRgba = icon_rgba --- Extract full pack into cache under {cacheRoot}/pokemon/. function PokemonExtract.run(rom, cache, opts) @@ -669,13 +686,7 @@ function PokemonExtract.run(rom, cache, opts) local nameBase = Versions.SPECIES_NAMES local infoBase = Versions.SPECIES_INFO local natBase = Versions.SPECIES_TO_NATIONAL - local w = Versions.MON_ICON_W or 32 - local h = Versions.MON_ICON_H or 32 - local iconH = h * 2 -- 64 (2 frames) - local iconBytes = Versions.MON_ICON_BYTES or math.floor(w * iconH / 2) -- 1024 for 32x64 - local palIdxBase = Versions.MON_ICON_PAL_INDICES local pals = load_icon_pals(rom) - local iconTable = Versions.MON_ICON_TABLE local frontPicTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_front_pic_table) or 0x2350AC local backPicTable = Versions.MON_BACK_PIC_TABLE or 0x23654C local palTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_palette_table) or 0x23730C @@ -727,20 +738,7 @@ function PokemonExtract.run(rom, cache, opts) -- Table omits SPECIES_NONE; SpeciesToNationalPokedexNum uses [species - 1]. toNat[sp] = (sp >= 1) and rom:u16(natBase + (sp - 1) * 2) or 0 - local ptr = rom:u32(iconTable + sp * 4) - local off = gba_off(ptr) - local palIdx = rom:get(palIdxBase + sp) or 0 - if palIdx >= Versions.MON_ICON_PAL_COUNT then palIdx = 0 end - local pal = pals[palIdx] or pals[0] - local rgba - if off then - local bytes = rom:readBytes(off, iconBytes) - local pixels = decode_4bpp(bytes, w, iconH) - rgba = bake_icon_rgba(pixels, pal, w, iconH) - else - rgba = string.rep(string.char(0, 0, 0, 0), w * iconH * 4) - end - put(cache, root .. "/icons/" .. sp .. ".rgba", rgba) + put(cache, root .. "/icons/" .. sp .. ".rgba", icon_rgba(rom, sp, pals)) picsWritten.icons = picsWritten.icons + 1 -- Front Pic (64x64 RGBA) diff --git a/src/import/gba/versions.lua b/src/import/gba/versions.lua index 0294e685..ee5dff12 100644 --- a/src/import/gba/versions.lua +++ b/src/import/gba/versions.lua @@ -37,7 +37,9 @@ Versions.ROM_SIZE = 16777216 -- setptrbyte each carry a leading byte plus a word (were shorter). The -- old sizes mis-decoded every instruction after one, so every cached -- script is stale. -Versions.CACHE_VERSION = 113 +-- v114: pokemon/icons/412.rgba, the SPECIES_EGG menu icon — eggs were drawn +-- with the icon of the species they hatch into. +Versions.CACHE_VERSION = 114 Versions.NATIVE_VERSION = 6 Versions.OW_VERSION = 1 Versions.ANIM_VERSION = 1 diff --git a/src/ui/game3/box_storage_ui.lua b/src/ui/game3/box_storage_ui.lua index 93796ed9..14cacbf7 100644 --- a/src/ui/game3/box_storage_ui.lua +++ b/src/ui/game3/box_storage_ui.lua @@ -738,7 +738,7 @@ function BoxStorageUI.draw() local isHovered = (BoxStorageUI.cursorSlot == s and BoxStorageUI.mode ~= "party_drawer" and not BoxStorageUI.holdingMon) local bounceY = (isHovered and BoxStorageUI.hoverFrame == 1) and -2 or 0 local f = (isHovered and BoxStorageUI.hoverFrame == 1) and 1 or 0 - local sp = Pokemon.speciesOf(mon) + local sp = Pokemon.speciesOrEgg(mon) local icon = Pokemon.icon(sp) if icon and icon.image then @@ -804,7 +804,7 @@ function BoxStorageUI.draw() -- If holding a mon, draw floating mini-icon under hand cursor if BoxStorageUI.holdingMon then - local hSp = Pokemon.speciesOf(BoxStorageUI.holdingMon) + local hSp = Pokemon.speciesOrEgg(BoxStorageUI.holdingMon) local hIcon = Pokemon.icon(hSp) if hIcon and hIcon.image then local q = hIcon.quads and hIcon.quads[0] diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index 2af1b24d..f6782538 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -446,7 +446,7 @@ local function ensure_slot_sprites(i, mon, selected) local maxHp = tonumber(mon.maxHp) or tonumber(mon.maxhp) or 1 local hpLevel = get_hp_bar_level(hp, maxHp, mon.isEgg) - local icon = Pokemon.icon(Pokemon.speciesOf(mon)) + local icon = Pokemon.icon(Pokemon.speciesOrEgg(mon)) local q0 = icon and icon.quads and icon.quads[0] if not slot.mon then local id = select(1, Oam.createSprite({ @@ -2111,10 +2111,17 @@ local function draw_filled_slot(i, mon, selected) local info = slot_info(i) local desc = slot_description(i, mon) - PartyChrome.drawSlot(win.kind, win.left, win.top, selected, desc ~= nil) + -- pokefirered/src/party_menu.c:781 DisplayPartyPokemonData: an egg's slot + -- has no HP frame and shows only its nickname (gText_EggNickname). + local isEgg = Pokemon.isEgg(mon) + PartyChrome.drawSlot(win.kind, win.left, win.top, selected, desc ~= nil or isEgg) local name = Pokemon.displayName(mon) party_print(name, baseX + info.nick[1], baseY + info.nick[2], 56) + if isEgg then + if desc then party_print(desc, baseX + info.desc[1], baseY + info.desc[2], 64) end + return + end party_print("Lv" .. tostring(mon.level or 0), baseX + info.level[1], baseY + info.level[2], 32) local gender = mon.gender or (Pokemon.gender and Pokemon.gender(mon.species, mon.personality)) diff --git a/src/ui/game3/pc_chrome.lua b/src/ui/game3/pc_chrome.lua index 553c9340..b41e5023 100644 --- a/src/ui/game3/pc_chrome.lua +++ b/src/ui/game3/pc_chrome.lua @@ -205,8 +205,8 @@ function PcChrome.drawLeftDataPanel(hoveredMon, hoverFrame) if not hoveredMon then return end -- 1. Front Sprite in TV Screen (X: 10..73, Y: 19..80, W: 64, H: 61) - local sp = Pokemon.speciesOf(hoveredMon) - local sprite = Pokemon.frontPic(sp) + -- pokefirered/src/pokemon_storage_system_data.c:1034, :1057 MON_DATA_SPECIES_OR_EGG + local sprite = Pokemon.frontPic(Pokemon.speciesOrEgg(hoveredMon)) if sprite and sprite.image then love.graphics.setColor(1, 1, 1, 1) local sw, sh = sprite.image:getDimensions() @@ -229,6 +229,10 @@ function PcChrome.drawLeftDataPanel(hoveredMon, hoverFrame) end local lvl = hoveredMon.level or 5 local gender = hoveredMon.gender or (hoveredMon.personality and ((hoveredMon.personality % 256 < 127) and "F" or "M")) + -- pokefirered/src/pokemon_storage_system_data.c:1091: an egg shows only + -- gText_EggNickname; the species, gender/level and item lines stay blank. + local isEgg = Pokemon.isEgg(hoveredMon) + if isEgg then nick = Strings("EGG") end -- Line 1: Nickname or Species Name (FONT_NORMAL, Y: 88) FrlgFont.draw(nick:sub(1, 10), 6, 88, { @@ -236,32 +240,34 @@ function PcChrome.drawLeftDataPanel(hoveredMon, hoverFrame) colors = FrlgFont.COLOR.WHITE }) - -- Line 2: /Species Name (FONT_NORMAL, Y: 102) - FrlgFont.draw("/" .. spName:sub(1, 10), 6, 102, { - small = false, - colors = FrlgFont.COLOR.WHITE - }) + if not isEgg then + -- Line 2: /Species Name (FONT_NORMAL, Y: 102) + FrlgFont.draw("/" .. spName:sub(1, 10), 6, 102, { + small = false, + colors = FrlgFont.COLOR.WHITE + }) - -- Line 3: Gender & Level (FONT_NORMAL, Y: 116) - if gender == "M" or gender == "male" then - FrlgFont.draw("♂", 6, 116, { small = false, colors = FrlgFont.COLOR.MALE }) - FrlgFont.draw(Strings("Lv%s", tostring(lvl)), 18, 116, { small = false, colors = FrlgFont.COLOR.WHITE }) - elseif gender == "F" or gender == "female" then - FrlgFont.draw("♀", 6, 116, { small = false, colors = FrlgFont.COLOR.FEMALE }) - FrlgFont.draw(Strings("Lv%s", tostring(lvl)), 18, 116, { small = false, colors = FrlgFont.COLOR.WHITE }) - else - FrlgFont.draw(Strings("Lv%s", tostring(lvl)), 6, 116, { small = false, colors = FrlgFont.COLOR.WHITE }) - end + -- Line 3: Gender & Level (FONT_NORMAL, Y: 116) + if gender == "M" or gender == "male" then + FrlgFont.draw("♂", 6, 116, { small = false, colors = FrlgFont.COLOR.MALE }) + FrlgFont.draw(Strings("Lv%s", tostring(lvl)), 18, 116, { small = false, colors = FrlgFont.COLOR.WHITE }) + elseif gender == "F" or gender == "female" then + FrlgFont.draw("♀", 6, 116, { small = false, colors = FrlgFont.COLOR.FEMALE }) + FrlgFont.draw(Strings("Lv%s", tostring(lvl)), 18, 116, { small = false, colors = FrlgFont.COLOR.WHITE }) + else + FrlgFont.draw(Strings("Lv%s", tostring(lvl)), 6, 116, { small = false, colors = FrlgFont.COLOR.WHITE }) + end - -- Line 4: Held Item Name (if holding an item) (FONT_SMALL, Y: 132) - local held = hoveredMon.heldItem or hoveredMon.item - if held and held > 0 then - local heldName = ItemsData.displayName(held) - if heldName and heldName ~= "" and heldName ~= "NONE" then - FrlgFont.draw(heldName:sub(1, 10), 6, 132, { - small = true, - colors = FrlgFont.COLOR.WHITE - }) + -- Line 4: Held Item Name (if holding an item) (FONT_SMALL, Y: 132) + local held = hoveredMon.heldItem or hoveredMon.item + if held and held > 0 then + local heldName = ItemsData.displayName(held) + if heldName and heldName ~= "" and heldName ~= "NONE" then + FrlgFont.draw(heldName:sub(1, 10), 6, 132, { + small = true, + colors = FrlgFont.COLOR.WHITE + }) + end end end @@ -381,7 +387,7 @@ function PcChrome.drawPartyDrawer(party, partyCursor, hoverFrame, holdingSource) local isLeadPickedUp = (holdingSource and holdingSource.loc == "party" and holdingSource.slot == 1) local leadMon = (not isLeadPickedUp) and party[1] if leadMon then - local sp = Pokemon.speciesOf(leadMon) + local sp = Pokemon.speciesOrEgg(leadMon) local icon = Pokemon.icon(sp) if icon and icon.image then local isHovered = (partyCursor == 1) @@ -401,7 +407,7 @@ function PcChrome.drawPartyDrawer(party, partyCursor, hoverFrame, holdingSource) local isPickedUp = (holdingSource and holdingSource.loc == "party" and holdingSource.slot == p) local pMon = (not isPickedUp) and party[p] if pMon then - local sp = Pokemon.speciesOf(pMon) + local sp = Pokemon.speciesOrEgg(pMon) local icon = Pokemon.icon(sp) if icon and icon.image then local isHovered = (partyCursor == p) diff --git a/src/ui/game3/release_seq.lua b/src/ui/game3/release_seq.lua index 9a8411df..3bf4ffde 100644 --- a/src/ui/game3/release_seq.lua +++ b/src/ui/game3/release_seq.lua @@ -125,7 +125,7 @@ function ReleaseSeq.draw() local curX = ReleaseSeq.startX local curY = ReleaseSeq.startY - (progress * 40) -- float upward 40px - local icon = ReleaseSeq.mon and Pokemon.icon(Pokemon.speciesOf(ReleaseSeq.mon)) + local icon = ReleaseSeq.mon and Pokemon.icon(Pokemon.speciesOrEgg(ReleaseSeq.mon)) if icon and icon.image then local q = icon.quads and icon.quads[0] love.graphics.setColor(1, 1, 1, alpha) diff --git a/src/ui/game3/summary_menu.lua b/src/ui/game3/summary_menu.lua index fb18e4b1..a969188b 100644 --- a/src/ui/game3/summary_menu.lua +++ b/src/ui/game3/summary_menu.lua @@ -56,7 +56,7 @@ end -- pokefirered/src/pokemon_summary_screen.c:5180 local function play_mon_cry() local mon = current_mon() - if not mon or mon.isEgg then return end + if not mon or Pokemon.isEgg(mon) then return end local species = Pokemon.speciesOf(mon) if not species then return end local okA, Audio = pcall(require, "src.core.game3.audio") @@ -153,7 +153,7 @@ function SummaryMenu.openMenu(party, startIndex, opts) SummaryMenu._slide.active = false local mon = current_mon() - if mon and mon.isEgg then + if mon and Pokemon.isEgg(mon) then SummaryMenu._page = PAGE_EGG elseif SummaryMenu._mode == "select_move" then SummaryMenu._page = PAGE_MOVES_INFO @@ -194,7 +194,7 @@ local function change_mon(delta) SummaryMenu._moveCursor = 1 SummaryMenu._swapSlot = nil local mon = current_mon() - if mon and mon.isEgg then + if mon and Pokemon.isEgg(mon) then SummaryMenu._page = PAGE_EGG elseif SummaryMenu._page == PAGE_EGG then SummaryMenu._page = PAGE_INFO @@ -676,7 +676,8 @@ local function draw_page_moves(mon, isDetail) end local function draw_page_egg(mon) - local species = Pokemon.speciesOf(mon) + -- pokefirered/src/pokemon_summary_screen.c:4016 MON_DATA_SPECIES_OR_EGG + local species = Pokemon.speciesOrEgg(mon) local nx, ny = cxy("name", 40, 18) draw_text(Strings("EGG"), nx, ny, 64, "NORMAL") @@ -759,7 +760,7 @@ function SummaryMenu.draw() end -- 2. OVERLAY LAYER (Foreground elements anchored to screen coordinates) - draw_top_bar_text(SummaryMenu._page, mon.isEgg) + draw_top_bar_text(SummaryMenu._page, Pokemon.isEgg(mon)) if SummaryMenu._page == PAGE_EGG then draw_page_egg(mon) else diff --git a/src/ui/game3/trade_scene.lua b/src/ui/game3/trade_scene.lua index ca7e9374..d8015ca3 100644 --- a/src/ui/game3/trade_scene.lua +++ b/src/ui/game3/trade_scene.lua @@ -257,23 +257,24 @@ function TradeSceneUi.draw() love.graphics.draw(art.linkMonShadow, 129 - w2, (s.crossMonBy or -10) + (s.crossY2b or 0) - h2) end + -- pokefirered/src/trade_scene.c:757 draws every mon by MON_DATA_SPECIES_OR_EGG -- pokefirered/src/trade_scene.c:1541 if s.crossMonVisible then - draw_mon(Pokemon.frontPic(s.offer and (s.offer.species or s.offer.speciesId)), + draw_mon(Pokemon.frontPic(Pokemon.speciesOrEgg(s.offer)), 60, 192 + (s.monY2a or 0), 1) - draw_mon(Pokemon.frontPic(s.received and (s.received.species or s.received.speciesId)), + draw_mon(Pokemon.frontPic(Pokemon.speciesOrEgg(s.received)), 180, -32 + (s.monY2b or 0), 1) end -- pokefirered/src/trade_scene.c:772 if s.playerVisible then - draw_mon(Pokemon.frontPic(s.offer and (s.offer.species or s.offer.speciesId)), + draw_mon(Pokemon.frontPic(Pokemon.speciesOrEgg(s.offer)), 120 + (s.monX2 or 0), 60, s.monScale or 1) end -- pokefirered/src/trade_scene.c:1716 if s.partnerVisible then - draw_mon(Pokemon.frontPic(s.received and (s.received.species or s.received.speciesId)), + draw_mon(Pokemon.frontPic(Pokemon.speciesOrEgg(s.received)), 120, 60, 1) end diff --git a/tests/drivers/game3_import2_fresh_cache.lua b/tests/drivers/game3_import2_fresh_cache.lua index ce6dff15..76dc14f3 100644 --- a/tests/drivers/game3_import2_fresh_cache.lua +++ b/tests/drivers/game3_import2_fresh_cache.lua @@ -42,6 +42,7 @@ local ROUND2_SAMPLE = { "data/generated/gba/move_relearner/bg.rgba", "data/generated/gba/pokemon/egg/hatch.rgba", "data/generated/gba/pokemon/front/412.rgba", + "data/generated/gba/pokemon/icons/412.rgba", "data/generated/gba/native/layouts/alt_366.mid", } diff --git a/tests/engine/game3_egg_display_name_test.lua b/tests/engine/game3_egg_display_name_test.lua new file mode 100644 index 00000000..994d3e77 --- /dev/null +++ b/tests/engine/game3_egg_display_name_test.lua @@ -0,0 +1,44 @@ +#!/usr/bin/env luajit +-- An egg is named by the language's own EGG, as pret's GetMonData(MON_DATA_NICKNAME) +-- does for any egg (pokefirered/src/pokemon.c:3020): the party and every screen +-- that names a mon through Pokemon.displayName / displayMonName show it, whatever +-- placeholder the egg's nickname stores. + +package.path = "./?.lua;./?/init.lua;" .. package.path +love = require("tests.love_stub") + +local T = require("tests.harness") +local check = T.check + +local Strings = require("src.core.Strings") +local Pokemon = require("src.core.game3.pokemon") + +-- A minimal pack, so no ROM is needed. +Pokemon._names = { [25] = "PIKACHU", [172] = "PICHU" } + +local egg = { species = 172, isEgg = true, nickname = "EGG", name = "EGG" } +local hatched = { species = 172, isEgg = false, nickname = "", name = "PICHU" } +local named = { species = 25, nickname = "SPARKY" } + +Strings.load({}) +check(Pokemon.displayName(egg) == "EGG", "an egg reads EGG with no catalog") +check(Pokemon.displayMonName(egg) == "EGG", "in both name helpers") + +Strings.load({ strings = { EGG = "OEUF" } }) +check(Pokemon.displayName(egg) == "OEUF", "an egg follows the catalog's EGG") +check(Pokemon.displayMonName(egg) == "OEUF", "in both name helpers") +check(egg.nickname == "EGG", "without its stored nickname being rewritten") + +local bare = { species = 172, isEgg = true, nickname = "" } +check(Pokemon.displayName(bare) == "OEUF", "an egg with no nickname at all is still named EGG") +local species_only = { species = 412, nickname = "" } +check(Pokemon.displayName(species_only) == "OEUF", "and so is one known only by the egg species") + +-- (looking up a species the pack lacks reloads it; put the minimal one back) +Pokemon._names = { [25] = "PIKACHU", [172] = "PICHU" } +check(Pokemon.displayName(hatched) == "PICHU", "a hatched egg goes back to its species name") +check(Pokemon.displayName(named) == "SPARKY", "and a nicknamed mon keeps its nickname") +check(Pokemon.displayMonName(named) == "SPARKY", "in both name helpers") + +Strings.load({}) +T.finish("game3_egg_display_name_test") diff --git a/tests/engine/game3_egg_icon_species_test.lua b/tests/engine/game3_egg_icon_species_test.lua new file mode 100644 index 00000000..744f7538 --- /dev/null +++ b/tests/engine/game3_egg_icon_species_test.lua @@ -0,0 +1,38 @@ +#!/usr/bin/env luajit +-- An egg's menu icon is SPECIES_EGG's, as pret draws it from +-- GetMonData(MON_DATA_SPECIES_OR_EGG) (pokefirered/src/pokemon.c:3245, +-- party_menu.c:2655), not the icon of the species it will hatch into. + +package.path = "./?.lua;./?/init.lua;" .. package.path +love = require("tests.love_stub") + +local T = require("tests.harness") +local check = T.check + +local Pokemon = require("src.core.game3.pokemon") +Pokemon._names = { [25] = "PIKACHU", [172] = "PICHU" } -- a minimal pack, so no ROM is needed + +check(Pokemon.speciesOrEgg({ species = 172, isEgg = true }) == 412, "a Pichu egg's icon is the EGG's") +check(Pokemon.speciesOrEgg({ species = 172, egg = true }) == 412, "whichever egg flag it carries") +check(Pokemon.speciesOrEgg({ species = 412 }) == 412, "an egg known only by the egg species stays one") +check(Pokemon.speciesOrEgg({ species = 172, isEgg = false }) == 172, "a hatched mon keeps its species") +check(Pokemon.speciesOrEgg({ species = 25 }) == 25, "and so does any other mon") +check(Pokemon.speciesOrEgg(nil) == nil, "no mon, no species") + +-- Every screen that draws a party or box mon's icon goes through it (the PC +-- chrome's third call is its hovered-mon front pic). +local SITES = { + { "src/ui/game3/party_menu.lua", 1 }, + { "src/ui/game3/box_storage_ui.lua", 2 }, + { "src/ui/game3/pc_chrome.lua", 3 }, + { "src/ui/game3/release_seq.lua", 1 }, +} +for _, site in ipairs(SITES) do + local f = assert(io.open(site[1], "rb")) + local src = f:read("*a") + f:close() + local n = select(2, src:gsub("Pokemon%.speciesOrEgg%(", "")) + check(n == site[2], ("%s draws %d mon(s) by speciesOrEgg (found %d)"):format(site[1], site[2], n)) +end + +T.finish("game3_egg_icon_species_test") diff --git a/tests/engine/game3_egg_screens_test.lua b/tests/engine/game3_egg_screens_test.lua new file mode 100644 index 00000000..e5fa3191 --- /dev/null +++ b/tests/engine/game3_egg_screens_test.lua @@ -0,0 +1,135 @@ +#!/usr/bin/env luajit +-- The screens that show a single mon draw an egg as an egg, as pret does with +-- GetMonData(MON_DATA_SPECIES_OR_EGG) and MON_DATA_NICKNAME +-- (pokefirered/src/pokemon.c:3245, :3020): the party slots, the PC's hovered-mon +-- panel, the summary's egg page, the trade scene, and the script string buffers. + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check = T.check + +require("src.core.GameVersion").set("firered") +package.loaded["src.core.game3.audio"] = setmetatable({}, { __index = function() return function() end end }) + +local gfx = setmetatable({}, { __index = function() return function() end end }) +gfx.newQuad = function() return {} end +gfx.newImage = function() return nil end +gfx.getWidth = function() return 240 end +gfx.getHeight = function() return 160 end +_G.love = { graphics = gfx, timer = { getTime = function() return 0 end } } + +local Strings = require("src.core.Strings") +local Pokemon = require("src.core.game3.pokemon") +Pokemon._names = { [25] = "PIKACHU", [172] = "PICHU" } -- a minimal pack, so no ROM is needed +Strings.load({ strings = { EGG = "OEUF" } }) + +local pics = {} +Pokemon.frontPic = function(species) + pics[#pics + 1] = species + return { image = { getDimensions = function() return 64, 64 end }, w = 64, h = 64 } +end +local texts = {} +local FrlgFont = require("src.ui.game3.frlg_font") +FrlgFont.draw = function(text) texts[#texts + 1] = tostring(text) end + +local function egg() + return { species = 172, isEgg = true, nickname = "EGG", name = "EGG", level = 5, + heldItem = 1, gender = "M", personality = 1, otName = "RED", otId = 1, + hp = 1, maxHp = 1, moves = {}, pp = {} } +end +local function has(list, value) + for _, v in ipairs(list) do if v == value then return true end end + return false +end + +-- pokefirered/src/pokemon_storage_system_data.c:1034, :1091 +local PcChrome = require("src.ui.game3.pc_chrome") +PcChrome.ensure = function() end +PcChrome.drawWaveforms = function() end +pics, texts = {}, {} +PcChrome.drawLeftDataPanel(egg(), 0) +check(pics[1] == 412, "the PC panel draws an egg's front pic as the EGG's (got " .. tostring(pics[1]) .. ")") +check(texts[1] == "OEUF", "and names it by the language's EGG (got " .. tostring(texts[1]) .. ")") +check(not has(texts, "/PICHU") and not has(texts, "Lv5"), + "with no species, level or item line: " .. table.concat(texts, " | ")) +pics, texts = {}, {} +PcChrome.drawLeftDataPanel({ species = 172, nickname = "PICHU", level = 5, gender = "M" }, 0) +check(pics[1] == 172 and has(texts, "/PICHU") and has(texts, "Lv5"), + "a hatched mon keeps its pic, species and level lines") + +-- pokefirered/src/party_menu.c:781 DisplayPartyPokemonData, :2197 sSlotTilemap_MainNoHP +local PartyMenu = require("src.ui.game3.party_menu") +local PartyChrome = require("src.ui.game3.party_chrome") +local slotsHidingHp = {} +PartyChrome.drawSlot = function(kind, _, _, _, hideHp) + if kind ~= "empty" then slotsHidingHp[#slotsHidingHp + 1] = hideHp and true or false end +end +texts, slotsHidingHp = {}, {} +PartyMenu.show({ { species = 25, nickname = "SPARKY", level = 12, gender = "M", hp = 30, maxHp = 30 }, egg() }) +PartyMenu.draw() +PartyMenu.close() +check(has(texts, "OEUF") and has(texts, "SPARKY"), "the party names both mons: " .. table.concat(texts, " | ")) +check(not has(texts, "Lv5") and has(texts, "Lv12"), "an egg's party slot shows no level, the other one does") +check(slotsHidingHp[1] == false and slotsHidingHp[2] == true, + "and an egg's slot has no HP frame (hideHp = " .. tostring(slotsHidingHp[1]) .. ", " .. tostring(slotsHidingHp[2]) .. ")") + +-- pokefirered/src/pokemon_summary_screen.c:4016 +local SummaryMenu = require("src.ui.game3.summary_menu") +local SummaryChrome = require("src.ui.game3.summary_chrome") +for k, v in pairs(SummaryChrome) do + if type(v) == "function" and k:match("^draw") then SummaryChrome[k] = function() end end +end +pics = {} +SummaryMenu.openMenu({ egg() }, 1, { page = 0 }) +SummaryMenu.draw() +SummaryMenu.close() +check(has(pics, 412) and not has(pics, 172), + "the summary's egg page draws the EGG's front pic (got " .. table.concat(pics, ",") .. ")") +pics = {} +local flagOnly = egg() +flagOnly.isEgg, flagOnly.egg = nil, true +SummaryMenu.openMenu({ flagOnly }, 1, { page = 0 }) +SummaryMenu.draw() +SummaryMenu.close() +check(has(pics, 412) and not has(pics, 172), "for any egg flag (got " .. table.concat(pics, ",") .. ")") + +-- pokefirered/src/trade_scene.c:757, :1239 +local TradeSceneUi = require("src.ui.game3.trade_scene") +local offer, received = egg(), { species = 25, nickname = "SPARKY" } +TradeSceneUi._core = { state = function() + return { playerVisible = true, partnerVisible = true, offer = offer, received = received } +end } +pics = {} +TradeSceneUi.draw() +check(pics[1] == 412 and pics[2] == 25, + "the trade scene draws a sent egg as the EGG (got " .. table.concat(pics, ",") .. ")") +TradeSceneUi._core = nil +local savedLove = _G.love +_G.love = nil +local TradeScene = require("src.core.game3.trade_scene") +TradeScene.play(offer, received, nil, {}) +local st = TradeScene.state() +check(st.sentName == "OEUF", "and names it by the language's EGG (got " .. tostring(st.sentName) .. ")") +check(st.recvName == "SPARKY", "while the other mon keeps its nickname") +_G.love = savedLove + +-- pokefirered/src/field_specials.c:1671 BufferMonNickname, daycare.c:1216 +local Ctx = require("src.core.game3.scripting.ctx") +local Flags = require("src.core.game3.scripting.flags") +local Std = require("src.core.game3.scripting.stdscripts") +local Natives = require("src.core.game3.scripting.natives") +package.loaded["src.core.game3.runtime"] = { + getSession = function() return { party = { egg() } } end, + isActive = function() return true end, +} +local ctx = Ctx.new({}) +ctx.mode, ctx.status = "bytecode", "running" +Flags.setVar(nil, ctx, 0x8004, 0) +Natives.special(ctx, Std.SPECIAL.GetSelectedMonNicknameAndSpecies, { log = function() end }) +check(ctx.stringVars and ctx.stringVars[1] == "OEUF", + "a script buffering an egg's nickname gets the language's EGG (got " + .. tostring(ctx.stringVars and ctx.stringVars[1]) .. ")") + +Strings.load({}) +T.finish("game3_egg_screens_test") diff --git a/tests/game3_import2_cache_version_test.lua b/tests/game3_import2_cache_version_test.lua index 9830444f..adcdb34d 100644 --- a/tests/game3_import2_cache_version_test.lua +++ b/tests/game3_import2_cache_version_test.lua @@ -164,6 +164,7 @@ local ROUND2_KEYS = { "data/generated/gba/pokemon/egg/hatch.rgba", "data/generated/gba/pokemon/egg/shard.rgba", "data/generated/gba/pokemon/front/412.rgba", + "data/generated/gba/pokemon/icons/412.rgba", -- src/trainer_card.c:265, :1454, :1560 "data/generated/gba/trainer_card/front_0.rgba", "data/generated/gba/trainer_card/front_4_female.rgba", diff --git a/tests/game3_import2_misc_assets_test.lua b/tests/game3_import2_misc_assets_test.lua index 54aa47c3..b26719a5 100644 --- a/tests/game3_import2_misc_assets_test.lua +++ b/tests/game3_import2_misc_assets_test.lua @@ -95,6 +95,7 @@ local WANT = { "data/generated/gba/pokemon/egg/hatch.rgba", "data/generated/gba/pokemon/egg/shard.rgba", "data/generated/gba/pokemon/front/412.rgba", + "data/generated/gba/pokemon/icons/412.rgba", "data/generated/gba/trainer_tower/manifest.lua", "data/generated/gba/trainer_tower/records_bg.rgba", "data/generated/gba/trainer_card/front_0.rgba", @@ -165,6 +166,8 @@ written["x/pokemon/egg/shard.rgba"] = "stub" written["x/pokemon/egg/manifest.lua"] = "stub" check(EggExtract.ready(stub, "x") == false, "egg ready() also wants the SPECIES_EGG front pic") written["x/pokemon/front/412.rgba"] = "stub" +check(EggExtract.ready(stub, "x") == false, "egg ready() also wants the SPECIES_EGG icon") +written["x/pokemon/icons/412.rgba"] = "stub" check(EggExtract.ready(stub, "x") == true, "egg ready() accepts the whole group") local regionFiles = {} @@ -234,6 +237,7 @@ local SHEETS = { { "pokemon/egg/hatch.rgba", 32, 128 }, { "pokemon/egg/shard.rgba", 32, 8 }, { "pokemon/front/412.rgba", 64, 64 }, + { "pokemon/icons/412.rgba", 32, 64 }, { "trainer_tower/records_bg.rgba", 240, 160 }, { "trainer_card/front_0.rgba", 240, 160 }, { "trainer_card/front_4_female.rgba", 240, 160 }, @@ -389,6 +393,8 @@ local eggPic = blobs["pokemon/front/412.rgba"] check(distinctColors(eggPic) > 4, "the EGG front pic is painted art") local _, _, _, eggCorner = pixel(eggPic, 64, 0, 0) eq(eggCorner, 0, "the EGG pic corner is transparent, as pic index 0 is") +local eggIcon = blobs["pokemon/icons/412.rgba"] +check(distinctColors(eggIcon) > 2, "the EGG menu icon is painted art") local eggMan = loadTable("pokemon/egg/manifest.lua") check(eggMan ~= nil and eggMan.hatch ~= nil and eggMan.hatch.frames == 4 and eggMan.hatch.sheetHeight == 128 and eggMan.shard.sheetWidth == 32,