-- Gen 2 #DEX, transcribed from engine/pokedex/pokedex.asm. -- -- The main screen is two layers, which is the thing to understand before any -- coordinate in here makes sense: -- -- * The **background** holds the frontpic box, the SEEN/OWN box, the -- vertical rule at column 8 and the bottom caption, and it is scrolled -- left by POKEDEX_SCX (5 pixels). So background tile column 0 lands at -- screen x -5. -- * The **window** holds the listing. Pokedex_InitMainScreen sets hWX to -- $47 (or $4a in OLD mode) and hWY to 0, so the window's own column 0 is -- at screen x 64 (67 in OLD mode) and it covers the full height. Twelve -- of its columns fit on screen, which is exactly the 11-wide list plus -- its scroll bar. -- -- Both layers are written through the same wTilemap buffer and copied to -- vBGMap0 / vBGMap1 at different times, which is why every hlcoord in the ASM -- reads as if it were the only screen. -- -- Everything the screens draw comes out of one 64-tile sheet decompressed -- over vTiles2 tile $31, plus an *inverted* font: Pokedex_LoadInvertedFont -- flips both bitplanes of the standard font, so under PREDEFPAL_POKEDEX -- (white, orange, dark red, black) the dex prints white on black. Here that -- is Chrome.printInverted, which draws the ordinary font page through the -- reversed palette; the inverted ' ' cell is a solid black tile no sheet -- carries, so PokedexMenu:blank paints it. -- -- Gold's dex sorts three ways -- NEW (Johto order), OLD (national, and the -- only mode that prints numbers) and A-Z. SELECT opens the option screen on -- the cart; here it cycles the mode directly. local Assets = require("src.render.Assets") local Chrome = require("src.ui.gen2.Chrome") local GbcPalette = require("src.render.GbcPalette") local Palettes = require("src.world.gen2.Palettes") local TileSheet = require("src.ui.gen2.TileSheet") local Nests = require("src.core.gen2.Nests") local Sound = require("src.core.Sound") local Unown = require("src.core.gen2.Unown") local PokedexMenu = {} PokedexMenu.__index = PokedexMenu PokedexMenu.isOpaque = true -- wDexListingHeight is set to 7 by Pokedex_InitMainScreen. local VISIBLE_ROWS = 7 local MODES = { "NEW", "OLD", "A-Z" } -- DexEntryScreen_ArrowCursorData, in its own order. PRNT is listed because the -- cursor stops on it; the port has no Game Boy Printer to send anything to. local ENTRY_ACTIONS = { "PAGE", "AREA", "CRY", "PRNT" } -- The four dwcoord columns the arrow parks in, row 17. local ENTRY_ACTION_X = { 1, 6, 11, 15 } -- engine/pokedex/pokedex.asm POKEDEX_SCX. local SCX = 5 -- hWX values, less the hardware's 7-pixel bias. local WINDOW_X = { NEW = 0x47 - 7, OLD = 0x4a - 7, ["A-Z"] = 0x47 - 7 } -- Tile ids out of the dex sheet, named after what the routines use them for. local TILE_BG = 0x32 local TILE_BORDER = { -- Pokedex_PlaceBorder topLeft = 0x33, top = 0x34, topRight = 0x35, left = 0x36, right = 0x37, bottomLeft = 0x38, bottom = 0x39, bottomRight = 0x3a, } local TILE_CAUGHT = 0x4f local TILE_FOOT = 0x5e local TILE_INCH = 0x5f local TILE_NO = { 0x5c, 0x5d } local TILE_DIVIDER = 0x61 local TILE_PAGE_TOP = 0x55 local TILE_PAGE_P = 0x56 local TILE_PAGE_DIGIT = { 0x57, 0x58 } -- String_SELECT_OPTION falls through into String_START_SEARCH with no -- terminator between them, so placing it at (1,17) writes all 18 tiles. local BOTTOM_CAPTION = { 0x3b, 0x48, 0x49, 0x4a, 0x44, 0x45, 0x46, 0x47, 0x3c, 0x3b, 0x41, 0x42, 0x43, 0x4b, 0x4c, 0x4d, 0x4e, 0x3c, } -- ...and the window gets its own copy of just the START > SEARCH half. local WINDOW_CAPTION = { 0x3c, 0x3b, 0x41, 0x42, 0x43, 0x4b, 0x4c, 0x4d, 0x4e, 0x3c } -- Pokedex_PutNewModeABCModeCursorOAM / Pokedex_PutOldModeCursorOAM, as -- { screen x, screen y, tile, xflip, yflip }. dbsprite emits y first and the -- values are OAM coordinates, so screen space is (x - 8, y - 16); the cursor -- row adds 16 pixels per step, which Pokedex_LoadCursorOAM does with a -- `swap a` on the low three bits. local function sprite(xTile, yTile, xPixel, yPixel, tile, xflip, yflip) return { x = xTile * 8 + xPixel - 8, y = yTile * 8 + yPixel - 16, tile = tile, xflip = xflip, yflip = yflip, } end local CURSOR_OAM = { sprite(9, 3, -1, 3, 0x30), sprite(9, 2, -1, 3, 0x31), sprite(10, 2, -1, 3, 0x32), sprite(11, 2, -1, 3, 0x32), sprite(12, 2, -1, 3, 0x33), sprite(16, 2, 0, 3, 0x33, true), sprite(17, 2, 0, 3, 0x32, true), sprite(18, 2, 0, 3, 0x32, true), sprite(19, 2, 0, 3, 0x31, true), sprite(19, 3, 0, 3, 0x30, true), sprite(9, 4, -1, 3, 0x30, false, true), sprite(9, 5, -1, 3, 0x31, false, true), sprite(10, 5, -1, 3, 0x32, false, true), sprite(11, 5, -1, 3, 0x32, false, true), sprite(12, 5, -1, 3, 0x33, false, true), sprite(16, 5, 0, 3, 0x33, true, true), sprite(17, 5, 0, 3, 0x32, true, true), sprite(18, 5, 0, 3, 0x32, true, true), sprite(19, 5, 0, 3, 0x31, true, true), sprite(19, 4, 0, 3, 0x30, true, true), } local CURSOR_OAM_OLD = { sprite(9, 3, -1, 0, 0x30), sprite(9, 2, -1, 0, 0x31), sprite(10, 2, -1, 0, 0x32), sprite(11, 2, -1, 0, 0x32), sprite(12, 2, -1, 0, 0x32), sprite(13, 2, -1, 0, 0x33), sprite(16, 2, -2, 0, 0x33, true), sprite(17, 2, -2, 0, 0x32, true), sprite(18, 2, -2, 0, 0x32, true), sprite(19, 2, -2, 0, 0x32, true), sprite(20, 2, -2, 0, 0x31, true), sprite(20, 3, -2, 0, 0x30, true), sprite(9, 4, -1, 0, 0x30, false, true), sprite(9, 5, -1, 0, 0x31, false, true), sprite(10, 5, -1, 0, 0x32, false, true), sprite(11, 5, -1, 0, 0x32, false, true), sprite(12, 5, -1, 0, 0x32, false, true), sprite(13, 5, -1, 0, 0x33, false, true), sprite(16, 5, -2, 0, 0x33, true, true), sprite(17, 5, -2, 0, 0x32, true, true), sprite(18, 5, -2, 0, 0x32, true, true), sprite(19, 5, -2, 0, 0x32, true, true), sprite(20, 5, -2, 0, 0x31, true, true), sprite(20, 4, -2, 0, 0x30, true, true), } -- Pokedex_PutScrollbarOAM: one OBJ, tile $0f, x 161, y from 20 to 141. local SCROLLBAR_TILE = 0x0f local SCROLLBAR_X = 161 - 8 local SCROLLBAR_TOP = 20 - 16 local SCROLLBAR_TRAVEL = 121 function PokedexMenu:wantsFillScale() return true end function PokedexMenu:drawsWidescreen() return true end -- opts: save, pokedex (pokedex.lua), pokemon (pokemon.lua), palettes, -- menuGfx (menu_gfx.lua), onClose(), entrySpecies (open straight on that -- species' ENTRY screen, the way `predef NewPokedexEntry` does), newEntry (the -- two-page NewPokedexEntry viewing, no action bar) function PokedexMenu.new(game, opts) opts = opts or {} local self = setmetatable({}, PokedexMenu) self.game = game self.save = opts.save or (game and game.save) local data = game and game.data or {} -- engine/pokedex/pokedex.asm:447 self.data = data self.dex = opts.pokedex or data.gen2Pokedex self.pokemon = opts.pokemon or data.pokemon self.palettes = opts.palettes or data.gen2Palettes self.onClose = opts.onClose self.modeIndex = 1 self.index = 1 self.scroll = 0 self.view = "list" -- list | entry | area | option | search | results | unown self.page = 1 self.entryAction = 1 self.picCache = {} -- The OPTION and SEARCH screens' own cursors, and wDexCurUnownIndex -- the -- slot UNOWN MODE's cursor sits on, 0-based like the cart's. self.optionIndex = 1 self.searchIndex = 1 self.unownIndex = 0 -- wDexSearchMonType1 starts at NORMAL + 1 and TYPE2 at 0 ("-----"). self.searchType = { 1, 0 } self.searchResults = nil local gfx = (opts.menuGfx or data.gen2MenuGfx or {}).pokedex self.gfx = gfx if gfx then self.sheet = TileSheet.new({ path = gfx.tiles, wide = gfx.tilesWide or 16, firstTile = gfx.firstTile or 0x31, palette = gfx.palette, }) self.objs = TileSheet.new({ path = gfx.objs, wide = gfx.objsWide or 16, firstTile = 0, palette = gfx.cursorPalette, }) -- Kept as the raw palette, not two colours: the COLOR option substitutes -- palettes at draw time, so picking colours out of one here would freeze -- the dex's ink and paper at whatever mode was live when it opened. self.dexPalette = gfx.palette end -- pokegold engine/pokegear/pokegear.asm Pokedex_GetArea: the AREA page -- draws through the Pokegear's own town-map tiles and TownMapPals, not the -- dex's PokedexLZ sheet. local mapGfx = (opts.menuGfx or data.gen2MenuGfx or {}).pokegear self.mapGfx = mapGfx if mapGfx then self.mapSheet = TileSheet.new({ path = mapGfx.tiles, wide = mapGfx.tilesWide or 16, firstTile = 0, paletteFor = function(tile) if not mapGfx.palettes then return nil end if tile >= 0x60 then return mapGfx.palettes[1] end return mapGfx.palettes[(mapGfx.palMap and mapGfx.palMap[tile + 1]) or 1] end, }) end -- Pokedex_LoadUnownFont: 27 tiles at vTiles2 tile FIRST_UNOWN_CHAR, live -- only while UNOWN MODE is on screen. It is a sheet rather than a font -- page (see PokedexMenu:unownGlyph), and it draws through the dex palette -- REVERSED, because the routine inverts the tiles on their way into VRAM -- the same way Pokedex_LoadInvertedFont inverts the ordinary font. local font = opts.font or data.font if font and font.imageUnown and gfx then self.unownFontBase = font.unownBase or 0x40 self.unownFont = TileSheet.new({ path = font.imageUnown, wide = font.unownWide or 3, firstTile = font.unownBase or 0x40, paletteFor = function() local pal = self.dexPalette if not pal then return nil end return { pal[4], pal[3], pal[2], pal[1] } end, }) end self:rebuild() -- NewPokedexEntry (engine/items/item_effects.asm:534-542): catching a mon the -- player did not already own runs the dex straight into that species' ENTRY -- screen rather than the listing. Set after rebuild because the row index is -- only meaningful once the rows exist. if opts.entrySpecies then for index, row in ipairs(self.rows) do if row.species == opts.entrySpecies then self.index = index self.view = "entry" self.page = 1 self:ensureVisible() break end end -- _NewPokedexEntry's own tail: `ld a, [wCurPartySpecies] / call PlayMonCry` -- (engine/pokedex/pokedex.asm:2554-2555). if opts.newEntry and self.view == "entry" then self.newEntry = true self:playCry(opts.entrySpecies) end end return self end function PokedexMenu:mode() return MODES[self.modeIndex] end function PokedexMenu:styled() return self.sheet ~= nil and self.sheet:available() end -- The species list in the current sort order. OLD is the national (index) -- order, which for Gen 2 is just speciesOrder. function PokedexMenu:order() local dex = self.dex if not dex then return {} end local mode = self:mode() if mode == "NEW" and dex.newOrder then return dex.newOrder end if mode == "A-Z" and dex.alphabeticalOrder then return dex.alphabeticalOrder end local out = {} for species, entry in pairs(dex.entries or {}) do out[entry.dex or #out + 1] = species end return out end function PokedexMenu:rebuild() local seen = (self.save and self.save.pokedex and self.save.pokedex.seen) or {} local caught = (self.save and self.save.pokedex and self.save.pokedex.caught) or {} local rows = {} for _, species in ipairs(self:order()) do local entry = self.dex and self.dex.entries and self.dex.entries[species] if entry then rows[#rows + 1] = { species = species, dex = entry.dex, seen = seen[species] == true, caught = caught[species] == true, } end end self.rows = rows self.index = math.max(1, math.min(self.index, math.max(1, #rows))) self:ensureVisible() end function PokedexMenu:ensureVisible() if self.index <= self.scroll then self.scroll = self.index - 1 elseif self.index > self.scroll + VISIBLE_ROWS then self.scroll = self.index - VISIBLE_ROWS end self.scroll = math.max(0, math.min(self.scroll, math.max(0, #self.rows - VISIBLE_ROWS))) end function PokedexMenu:current() return self.rows[self.index] end function PokedexMenu:totals() local seen, caught = 0, 0 for _, entry in ipairs(self.rows) do if entry.seen then seen = seen + 1 end if entry.caught then caught = caught + 1 end end return seen, caught end -- The entry bar's arrow flashes. Counted here rather than in the entry -- branch so it keeps ticking on a frame with no input, and on the same 32-step -- period the AREA map's own blink uses (PokedexMenu:updateArea) so the two -- cannot drift out of step on screens that show both. function PokedexMenu:cursorVisible() return ((self.entryBlink or 0) % 32) < 20 end function PokedexMenu:update(_dt) self.entryBlink = (self.entryBlink or 0) + 1 local input = self.game and self.game.input if not input then return end if self.view == "entry" then -- NewPokedexEntry is two WaitPressAorB_BlinkCursor pages and then out -- (engine/pokedex/new_pokedex_entry.asm:19-23). if self.newEntry then if input:wasPressed("a") or input:wasPressed("b") then if self.page == 1 then self.page = 2 elseif self.onClose then self.onClose() end end return end -- DexEntryScreen_MenuActionJumptable's PAGE flips between the entry's two -- pages; B and A back out to the listing. -- DexEntryScreen_ArrowCursorData: LEFT/RIGHT walk an arrow across four -- actions at (1,17) (6,17) (11,17) (15,17), and A runs the one under it. -- This used to flip the page on LEFT/RIGHT directly, which is a shortcut -- that reads fine and quietly makes three of the four actions unreachable -- -- AREA among them, so the nest map could never be opened at all. if input:wasPressed("right") then self.entryAction = (self.entryAction % #ENTRY_ACTIONS) + 1 elseif input:wasPressed("left") then self.entryAction = (self.entryAction - 2) % #ENTRY_ACTIONS + 1 elseif input:wasPressed("a") then local action = ENTRY_ACTIONS[self.entryAction] if action == "PAGE" then self.page = self.page == 1 and 2 or 1 elseif action == "AREA" then self.view = "area" self.areaRegion = nil elseif action == "CRY" then self:playCry(self:current() and self:current().species) elseif action == "PRNT" then self:printEntry() end elseif input:wasPressed("b") then self.view = "list" end return end if self.view == "area" then return self:updateArea(input) end if self.view == "option" then return self:updateOption(input) end if self.view == "search" then return self:updateSearch(input) end if self.view == "unown" then return self:updateUnown(input) end if input:wasPressed("b") then if self.onClose then self.onClose() end return elseif input:wasPressed("select") then -- Pokedex_UpdateMainScreen: SELECT opens the OPTION screen and START the -- SEARCH screen; neither cycles anything in place. self.view = "option" self.optionIndex = self.modeIndex return elseif input:wasPressed("start") then self.view = "search" self.searchIndex = 1 self.searchType = self.searchType or { 1, 0 } self.searchResults = nil return elseif input:wasPressed("up") then self.index = self.index > 1 and self.index - 1 or #self.rows self:ensureVisible() return elseif input:wasPressed("down") then self.index = self.index < #self.rows and self.index + 1 or 1 self:ensureVisible() return elseif input:wasPressed("a") then local row = self:current() -- Pokedex_UpdateMainScreen's .a returns unless the mon has been seen. if row and row.seen then self.view = "entry" self.page = 1 -- Pokedex_InitDexEntryScreen's tail cries the selected mon -- (pokedex.asm:345-347); PAGE itself does not (:386-394). self:playCry(row.species) end return end end function PokedexMenu:monName(species) local def = self.pokemon and self.pokemon[species] return (def and def.name) or species end function PokedexMenu:picFor(species) local def = self.pokemon and self.pokemon[species] local path = def and def.spriteFront -- Pokedex_LoadSelectedMonTiles (engine/pokedex/pokedex.asm:2364) copies -- wFirstUnownSeen into wUnownLetter before GetMonFrontpic, so the #DEX shows -- the form the player FIRST met. The species' own row is letter A's pic -- (src/import/RomExtractorGen2.lua fills it that way), which is exactly what -- the cart draws while the byte is still 0. if species == Unown.SPECIES then local first = self.save and tonumber(self.save.firstUnownSeen) or 0 if first ~= 0 then path = Unown.formSprite(self.pokemon, first, false) or path end end if not path then return nil end local cached = self.picCache[path] if cached == nil then local ok, image = pcall(Assets.image, path) cached = ok and image or false self.picCache[path] = cached end return cached or nil end function PokedexMenu:questionMark() local path = self.gfx and self.gfx.questionMark if not path then return nil end local cached = self.picCache[path] if cached == nil then local ok, image = pcall(Assets.image, path) cached = ok and image or false self.picCache[path] = cached end return cached or nil end -- Pokedex_PlaceFrontpicTopLeftCorner lays a 7x7 block of tiles at (1,1) on -- every dex screen, and Pokedex_LoadSelectedMonTiles fills those 49 tiles -- -- with the mon's frontpic if it has been seen, with LoadQuestionMarkPic's if -- not. -- -- Gen 2 pics are 5x5, 6x6 or 7x7, and PadFrontpic centres the small ones in -- that block: a 6x6 goes one tile in from the top left, a 5x5 one across and -- two down. Everything else in the block stays blank, which under the -- palette in force is its colour 0 -- so the pic sits on a solid square of -- that colour rather than on the panel. -- -- `ownColors` picks which palette that is. _CGB_Pokedex_Init branches on -- wCurPartySpecies: Pokedex_InitMainScreen sets it to -1, so the listing -- draws every mon through PokedexQuestionMarkPalette (which is why the cart's -- main screen shows a green mon on green), while Pokedex_InitDexEntryScreen -- sets the real species and gets its own two colours. local PIC_PAD = { [7] = { 0, 0 }, [6] = { 1, 1 }, [5] = { 1, 2 } } function PokedexMenu:drawPic(row, tx, ty, ownColors) local G = love.graphics local image, colors if row and row.seen then image = self:picFor(row.species) if ownColors then colors = self.palettes and Palettes.monColors(self.palettes, row.species) else colors = self.gfx and self.gfx.questionMarkPalette end else image = self:questionMark() colors = self.gfx and self.gfx.questionMarkPalette end if not image then return end local blank = colors and GbcPalette.color(colors, 1) or { 255, 255, 255 } G.setColor(blank[1] / 255, blank[2] / 255, blank[3] / 255, 1) G.rectangle("fill", tx * 8, ty * 8, 7 * 8, 7 * 8) local tiles = math.floor(image:getWidth() / 8) local pad = PIC_PAD[tiles] or PIC_PAD[7] G.setColor(1, 1, 1, 1) local function body() G.draw(image, (tx + pad[1]) * 8, (ty + pad[2]) * 8) end if colors and GbcPalette.available() then GbcPalette.with(colors, body) else body() end end -- Pokedex_DrawFootprint: $62..$65 at (18,1), a 2x2 out of the strip -- Pokedex_LoadAnyFootprint would have requested into those tiles. function PokedexMenu:drawFootprint(species, tx, ty) local path = self.gfx and self.gfx.footprints local order = self.gfx and self.gfx.footprintOrder if not (path and order) then return end local index for i, id in ipairs(order) do if id == species then index = i break end end if not index then return end local cached = self.picCache[path] if cached == nil then local ok, image = pcall(Assets.image, path) cached = ok and image or false self.picCache[path] = cached end if not cached then return end local quad = self.footprintQuad if not quad then quad = love.graphics.newQuad(0, 0, 16, 16, cached:getDimensions()) self.footprintQuad = quad end quad:setViewport(0, (index - 1) * 16, 16, 16, cached:getDimensions()) love.graphics.setColor(1, 1, 1, 1) local function body() love.graphics.draw(cached, quad, tx * 8, ty * 8) end if self.gfx.palette and GbcPalette.available() then GbcPalette.with(self.gfx.palette, body) else body() end end function PokedexMenu:tile(id, tx, ty) if self.sheet then self.sheet:draw(id, tx, ty) end end function PokedexMenu:fill(id, tx, ty, wide, high) for y = ty, ty + high - 1 do for x = tx, tx + wide - 1 do self:tile(id, x, y) end end end function PokedexMenu:text(str, tx, ty) Chrome.printInverted(str, tx, ty, self.gfx and self.gfx.palette) end -- The inverted font's ' ' cell. Uninverted it is shade 0 throughout, so -- inverted it is a solid shade 3 -- black under PREDEFPAL_POKEDEX -- and -- every ClearBox and box interior on these screens is made of it. function PokedexMenu:blank(tx, ty, wide, high) -- Colour 3, which is what an inverted shade-0 cell resolves to and the same -- colour Chrome.printThrough paints behind an inverted string. local paper = self.dexPalette and GbcPalette.color(self.dexPalette, 4) or { 0, 0, 0 } local G = love.graphics G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1) G.rectangle("fill", tx * 8, ty * 8, wide * 8, high * 8) G.setColor(1, 1, 1, 1) end -- Pokedex_PlaceBorder: b interior rows by c interior columns, so the box on -- screen is (c + 2) wide and (b + 2) tall. The interior is ' ' ($7f), which -- in the inverted font page is a solid black cell. function PokedexMenu:border(tx, ty, interiorRows, interiorCols) local B = TILE_BORDER self:tile(B.topLeft, tx, ty) for i = 1, interiorCols do self:tile(B.top, tx + i, ty) end self:tile(B.topRight, tx + interiorCols + 1, ty) self:blank(tx + 1, ty + 1, interiorCols, interiorRows) for row = 1, interiorRows do self:tile(B.left, tx, ty + row) self:tile(B.right, tx + interiorCols + 1, ty + row) end local bottom = ty + interiorRows + 1 self:tile(B.bottomLeft, tx, bottom) for i = 1, interiorCols do self:tile(B.bottom, tx + i, bottom) end self:tile(B.bottomRight, tx + interiorCols + 1, bottom) end -- PrintNum: a right-aligned field of `digits` characters, space-padded unless -- PRINTNUM_LEADINGZEROS was set. `before` splits the field into an integer -- part and a fraction with a '.' between them. local function printNumString(value, digits, leadingZeros, before) local text = ("%0" .. digits .. "d"):format(math.max(0, math.floor(value or 0))) if not leadingZeros then local kept = false local out = {} for i = 1, #text do local ch = text:sub(i, i) -- .PrintDigit stops suppressing once e runs out, which is the digit -- immediately before the decimal point (and the units digit when there -- is none), so those always print even as a zero. local forced = i == #text or (before and i >= before) if ch ~= "0" or kept or forced then kept = true out[#out + 1] = ch else out[#out + 1] = " " end end text = table.concat(out) end if before then return text:sub(1, before) .. "." .. text:sub(before + 1) end return text end -- ---------------------------------------------------------------- main screen -- Pokedex_DrawMainScreenBG, drawn behind the window and scrolled by SCX. function PokedexMenu:drawMainBackground() local G = love.graphics G.push() G.translate(-SCX, 0) -- One column past the screen: SCX uncovers five pixels of column 20, which -- the BG map holds the same background fill in. self:fill(TILE_BG, 0, 0, Chrome.SCREEN_W + 1, Chrome.SCREEN_H) self:border(0, 0, 7, 7) self:border(0, 9, 6, 7) local seen, caught = self:totals() self:text("SEEN", 1, 11) self:text(printNumString(seen, 3), 5, 12) self:text("OWN", 1, 14) self:text(printNumString(caught, 3), 5, 15) for i, id in ipairs(BOTTOM_CAPTION) do self:tile(id, i, 17) end -- The rule between the two halves of the background and the window. self:tile(0x59, 8, 0) for y = 1, 7 do self:tile(0x5a, 8, y) end self:tile(0x53, 8, 8) self:tile(0x54, 8, 9) for y = 10, 15 do self:tile(0x5a, 8, y) end self:tile(0x5b, 8, 16) self:drawPic(self:current(), 1, 1) G.pop() end -- DrawPokedexListWindow + Pokedex_PrintListing, on the window layer. hWX -- puts the window's column 0 at screen x 64 (67 in OLD mode) and only twelve -- of its columns fit, so everything past column 11 is off screen anyway. function PokedexMenu:drawMainWindow() local G = love.graphics local old = self:mode() == "OLD" G.push() G.translate(WINDOW_X[self:mode()] or 64, 0) -- ClearBox(0, 1) 15 rows by 11 columns, then the top and bottom edges. self:blank(0, 1, 11, 15) for x = 0, 10 do self:tile(TILE_BORDER.top, x, 0) self:tile(TILE_BORDER.bottom, x, 16) end self:tile(0x3f, 5, 0) self:tile(0x40, 5, 16) -- The scroll bar column, or its flat OLD-mode replacement. local top, mid, bottom = 0x50, 0x51, 0x52 if old then top, mid, bottom = 0x66, 0x67, 0x68 end self:tile(top, 11, 0) for y = 1, 15 do self:tile(mid, 11, y) end self:tile(bottom, 11, 16) self:fill(TILE_BG, 0, 17, 12, 1) for i, id in ipairs(WINDOW_CAPTION) do self:tile(id, i - 1, 17) end for row = 1, VISIBLE_ROWS do local entry = self.rows[row + self.scroll] if entry then local ty = row * 2 if old then self:text(printNumString(entry.dex or 0, 3, true), 0, ty - 1) end if entry.seen then if entry.caught then self:tile(TILE_CAUGHT, 0, ty) end self:text(self:monName(entry.species), 1, ty) else self:text("-----", 1, ty) end end end G.pop() end function PokedexMenu:drawCursorObjs() if not (self.objs and self.objs:available()) then -- Without the OBJ sheet, mark the row the way every other Gen 2 list does. local row = self.index - self.scroll Chrome.cursor(math.floor((WINDOW_X[self:mode()] or 64) / 8) - 1, row * 2) return end local G = love.graphics local table_ = self:mode() == "OLD" and CURSOR_OAM_OLD or CURSOR_OAM -- Pokedex_LoadCursorOAM adds (cursor & 7) * 16 to every y. local offset = ((self.index - self.scroll - 1) % 8) * 16 for _, obj in ipairs(table_) do local quad = self.objs:quad(obj.tile) local image = self.objs:image() if quad and image then G.setColor(1, 1, 1, 1) local sx = obj.xflip and -1 or 1 local sy = obj.yflip and -1 or 1 local ox = obj.xflip and 8 or 0 local oy = obj.yflip and 8 or 0 local function body() G.draw(image, quad, obj.x + ox, obj.y + offset + oy, 0, sx, sy) end if self.gfx.cursorPalette and GbcPalette.available() then GbcPalette.with(self.gfx.cursorPalette, body) else body() end end end -- The scroll bar thumb rides the whole list, not the page. Its OAM -- attribute byte is 0, so unlike the cursor it wears OBJ palette 0, which -- _CGB_Pokedex_Resume left as InitPartyMenuOBPals' first entry. local total = math.max(1, #self.rows - 1) local travel = math.floor((self.index - 1) * SCROLLBAR_TRAVEL / total) local quad = self.objs:quad(SCROLLBAR_TILE) if quad and self:mode() ~= "OLD" then G.setColor(1, 1, 1, 1) local pals = self.palettes and self.palettes.partyMenu local colors = pals and pals[1] local function body() G.draw(self.objs:image(), quad, SCROLLBAR_X, SCROLLBAR_TOP + travel) end if colors and GbcPalette.available() then GbcPalette.with(colors, body) else body() end end end function PokedexMenu:drawList() self:drawMainBackground() self:drawMainWindow() self:drawCursorObjs() end -- --------------------------------------------------------------- entry screen -- Pokedex_DrawDexEntryScreenBG + DisplayDexEntry. -- -- Pokedex_InitDexEntryScreen only pushes the window off screen (hWX $a7); it -- never resets hSCX, so this screen inherits the main screen's 5-pixel scroll -- and the rightmost five pixels are whatever the BG map holds past column 19. -- PlayMonCry, silent for a species with no extracted cry rather than raising. -- PRNT. Pokedex_Print (engine/pokedex/pokedex.asm) hands the entry to the -- Game Boy Printer over the link port, which is a device this port cannot -- have; Gen 1's Yellow dex answers the same problem the same way -- (src/ui/PokedexMenu.lua's PRNT arm), so Gold does too rather than leaving -- the fourth action inert: the page the printer would have printed is written -- out as a PNG under prints/ in the save folder, at the printer's own 160x144. -- -- The shot is THIS screen's own entry draw, so what lands in the file is what -- was on the dex when PRNT was pressed, page and all. function PokedexMenu:printEntry() local row = self:current() if not row then return end local Printer = require("src.core.Printer") local Strings = require("src.core.Strings") local TextBox = require("src.render.TextBox") local name = (self.pokemon and self.pokemon[row.species] and self.pokemon[row.species].name) or tostring(row.species) local saved, err = Printer.save("dex_" .. tostring(row.species), 160, 144, function() self:drawEntry() end) -- Word for word what Yellow's PRNT says (src/ui/PokedexMenu.lua), so the two -- generations share one catalog entry and a translation covers both. local text = saved and Strings("Printed %s's\ndata!\fSaved as\n%s\vin the save\nfolder.", name, tostring(saved)) or Strings("Printer error!\n%s", tostring(err)) if self.game and self.game.stack then self.game.stack:push(TextBox.new(self.game, text)) end end function PokedexMenu:playCry(species) if not species then return end local cries = self.data and self.data.audio and self.data.audio.cries if cries and cries[species] then Sound.playCry(self.data, species) end end -- ------------------------------------------------------------------- AREA -- -- Pokedex_GetArea (engine/pokegear/pokegear.asm) borrows the Pokegear's town -- map and overlays FindNest's landmarks. The region shown starts as the one the -- player is standing in; LEFT/RIGHT swap it, which is how you see a Kanto mon's -- nests from Johto. B goes back to the entry. function PokedexMenu:areaRegionName() if self.areaRegion then return self.areaRegion end local landmark = self:playerLandmark() return Nests.regionOf(landmark) or "johto" end -- The player's landmark index, which is what decides the starting region. function PokedexMenu:playerLandmark() local save = self.game and self.game.save local mapId = save and save.position and save.position.map local def = mapId and self.data and self.data.gen2Maps and self.data.gen2Maps[mapId] return def and def.landmark end -- The Pokegear's own tilemap blit: a flat list of tile ids, row-major over the -- 20x18 screen. Reimplemented here rather than reached for across modules -- because the dex draws through its own sheet and palette (the COLOR option -- substitutes them), and borrowing Pokegear's would freeze the map's ink. function PokedexMenu:drawTilemap(cells) if type(cells) ~= "table" then return end local sheet = self.mapSheet if not sheet then return end local i = 1 for ty = 0, Chrome.SCREEN_H - 1 do for tx = 0, Chrome.SCREEN_W - 1 do local id = cells[i] if id then sheet:draw(id, tx, ty) end i = i + 1 end end end function PokedexMenu:updateArea(input) self.areaBlink = (self.areaBlink or 0) + 1 if input:wasPressed("b") or input:wasPressed("a") then self.view = "entry" return end if input:wasPressed("left") or input:wasPressed("right") then self.areaRegion = (self:areaRegionName() == "johto") and "kanto" or "johto" end end function PokedexMenu:drawArea() local row = self:current() if not row then return end local region = self:areaRegionName() local save = self.game and self.game.save local nests = Nests.find(self.data, row.species, region, save) self:fill(TILE_BG, 0, 0, Chrome.SCREEN_W + 1, Chrome.SCREEN_H) -- The map itself is the Pokegear's, drawn through the same gfx the MAP card -- uses. Without it (a cache imported before the town map was extracted) the -- page still lists the landmark NAMES, which is the information the screen -- exists to convey. local maps = self.mapGfx and self.mapGfx.maps local cells = maps and maps[region] if cells then self:drawTilemap(cells) end self:blank(0, 0, Chrome.SCREEN_W, 2) self:text(self:monName(row.species) .. "'S NEST", 1, 0) self:text(region == "kanto" and "KANTO" or "JOHTO", 1, 1) local G = love.graphics if #nests == 0 then -- A species with no grass, water or roamer entry in this region. The cart -- simply shows the map with nothing blinking on it. self:text("AREA UNKNOWN", 4, 16) return end -- engine/pokegear/pokegear.asm:2427 local on = ((self.areaBlink or 0) % 32) < 20 if cells and on then for _, index in ipairs(nests) do local mark = Nests.landmark(self.data, index) if mark and mark.x and mark.y then G.setColor(0, 0, 0, 1) G.rectangle("fill", mark.x - 2, mark.y - 2, 5, 5) G.setColor(1, 1, 1, 1) G.rectangle("fill", mark.x - 1, mark.y - 1, 3, 3) end end end -- Name the first one in words as well as on the map: the flashing dot is -- unreadable at this size on a modern display, and the landmark name is what -- a player actually wants off this screen. local first = Nests.landmark(self.data, nests[1]) if first and first.name then local name = tostring(first.name):gsub("\n", " ") self:text(name, 1, 16) if #nests > 1 then self:text(("+%d"):format(#nests - 1), 17, 16) end end end function PokedexMenu:drawEntry() local row = self:current() if not row then return end local entry = self.dex and self.dex.entries and self.dex.entries[row.species] if not entry then return end local G = love.graphics G.push() G.translate(-SCX, 0) local ok, err = pcall(function() self:drawEntryBody(row, entry) end) G.pop() if not ok then error(err, 0) end end function PokedexMenu:drawEntryBody(row, entry) -- One column past the screen, because the scroll exposes it: the BG map is -- 32 tiles wide and the fill runs on past the 20 the screen shows, so the -- five pixels SCX uncovers on the right are the same background colour. self:fill(TILE_BG, 0, 0, Chrome.SCREEN_W + 1, Chrome.SCREEN_H) self:border(0, 0, 15, 18) -- The right border column is then erased: (19,0) becomes the top edge, -- rows 1-15 blank, and (19,16) the bottom edge. self:tile(TILE_BORDER.top, 19, 0) self:blank(19, 1, 1, 15) self:tile(TILE_BORDER.bottom, 19, 16) for x = 1, 19 do self:tile(TILE_DIVIDER, x, 10) end -- Pokedex_DrawDexEntryScreenBG blanks 18 columns, _NewPokedexEntry's own -- ByteFill 19 (pokedex.asm:1155-1157, :2540-2545). self:blank(1, 17, self.newEntry and 19 or 18, 1) self:tile(0x3b, 0, 17) -- _NewPokedexEntry ByteFills the action row away (pokedex.asm:2540-2545). if not self.newEntry then self:text(" PAGE AREA CRY PRNT", 1, 17) -- Pokedex_InitArrowCursor parks an arrow on the selected action; without it -- the four words are decoration and there is no way to tell what A will do. -- -- ENTRY_ACTION_X holds the ARROW's own dwcoord columns, so they are used as -- they are. Subtracting one put the arrow a column early every time: on -- PAGE it landed at column 0, outside the bar against the border, and on -- CRY it landed on the last "A" of AREA and ate it. The words sit one -- column right of each arrow slot -- the leading space in the string below -- is what makes that true -- so the raw value is already the gap in front of -- each word. -- -- Drawn through the dex's inverted palette and blinking, because this arrow -- is white on the dark bar rather than the black one every other Gold menu -- uses, and it flashes. The half-second period is chosen to read like the -- cart; the exact frame count is not cited here. if self:cursorVisible() then Chrome.cursorThrough(ENTRY_ACTION_X[self.entryAction or 1] or 1, 17, self.gfx and self.gfx.palette, true) end end self:drawPic(row, 1, 1, true) self:drawFootprint(row.species, 18, 1) self:text(self:monName(row.species), 9, 3) self:text(entry.kind or "", 9, 5) self:tile(TILE_NO[1], 2, 8) self:tile(TILE_NO[2], 3, 8) self:text(printNumString(entry.dex or 0, 3, true), 4, 8) -- .Height / .Weight are placeholder strings until the mon is caught: -- "HT ?'??"" at (9,7) and "WT ???lb" at (9,9). self:text("HT", 9, 7) self:text("WT", 9, 9) self:tile(TILE_FOOT, 14, 7) self:text("lb", 17, 9) if not row.caught then self:text(" ?", 11, 7) self:text("??", 15, 7) self:tile(TILE_INCH, 17, 7) self:text(" ???", 11, 9) return end -- The height word is four digits with two in front of the point and the -- point replaced by the foot mark; the weight word is five with four in -- front. Both are already the digits the cart prints. local height = printNumString(entry.height or 0, 4, false, 2) self:text(height:sub(1, 2), 12, 7) self:text(height:sub(4), 15, 7) self:tile(TILE_INCH, 17, 7) self:text(printNumString(entry.weight or 0, 5, false, 4), 11, 9) -- Page marker, then the description. ClearBox(2,11) is 5 rows by 18 -- columns and steps two rows, so the three lines land on 11/13/15. self:tile(TILE_PAGE_TOP, 1, 9) self:tile(TILE_PAGE_TOP, 2, 9) self:tile(TILE_PAGE_P, 1, 10) self:tile(TILE_PAGE_DIGIT[self.page] or TILE_PAGE_DIGIT[1], 2, 10) local text = self.page == 2 and entry.text2 or entry.text local ty = 11 for part in (tostring(text or "") .. ""):gmatch("(.-)") do if ty > 15 then break end self:text(part, 2, ty) ty = ty + 2 end end -- ------------------------------------------------------------------ fallbacks -- A cache from before the dex sheet was extracted has no `pokedex` table. -- Rather than draw nothing, fall back to the plain boxes this screen used -- before the sweep. function PokedexMenu:drawPlain() Chrome.clear() if self.view == "unown" then -- Without the dex sheet there are no border tiles, so the ring of letters -- is printed straight onto a plain box; the slot coordinates are the same -- ones the styled screen uses. Chrome.box(2, 1, 15, 13) local list = self:unownDex() local slot = math.min(self.unownIndex or 0, math.max(0, #list - 1)) for index, value in ipairs(list) do local coords = PokedexMenu.UNOWN_COORDS[index] if coords then Chrome.print(Unown.name(value) or "?", coords[1], coords[2]) if index == slot + 1 then Chrome.cursor(coords[3], coords[4]) end end end local letter = list[slot + 1] if letter then self:drawUnownPic(letter, 6, 5) Chrome.print(Unown.word(letter) or "", 4, 15) end return end if self.view == "entry" then local row = self:current() local entry = row and self.dex and self.dex.entries and self.dex.entries[row.species] Chrome.box(0, 0, 20, 10) if entry then Chrome.print(("%s %s"):format( Chrome.number(entry.dex or 0, 3, true), self:monName(row.species)), 1, 1) Chrome.print(entry.kind or "", 1, 3) Chrome.print("HT " .. printNumString(entry.height or 0, 4, false, 2), 1, 5) Chrome.print("WT " .. printNumString(entry.weight or 0, 5, false, 4), 1, 7) self:drawPic(row, 12, 1, true) Chrome.box(0, 10, 20, 8) local ty = 11 for part in ((entry.text or "") .. ""):gmatch("(.-)") do if ty > 16 then break end Chrome.print(part, 1, ty) ty = ty + 1 end end return end Chrome.box(0, 0, 13, 18) for row = 1, VISIBLE_ROWS do local i = row + self.scroll local entry = self.rows[i] if entry then local ty = 1 + (row - 1) * 2 if i == self.index then Chrome.cursor(0, ty) end Chrome.print(Chrome.number(entry.dex or 0, 3, true), 1, ty) Chrome.print(entry.seen and self:monName(entry.species) or "-----", 5, ty) end end Chrome.box(13, 0, 7, 11) self:drawPic(self:current(), 13, 1) Chrome.box(13, 11, 7, 7) local seen, caught = self:totals() Chrome.print(self:mode(), 14, 12) Chrome.print("SEEN", 14, 14) Chrome.printRight(tostring(seen), 19, 15) Chrome.print("OWN", 14, 16) Chrome.printRight(tostring(caught), 19, 17) end -- ---------------------------------------------------------- OPTION / SEARCH -- -- Pokedex_DrawOptionScreenBG and Pokedex_DrawSearchScreenBG, transcribed at -- their own hlcoords. Both draw over the dex's own background fill rather -- than into a text box, which is why they use `border` and `text` here. -- The SEARCH screen's type wheel: "-----" is index 0 and the eighteen real -- types follow, in the order data/types/names.asm lists them. PokedexMenu.SEARCH_TYPES = { "NORMAL", "FIGHTING", "FLYING", "POISON", "GROUND", "ROCK", "BUG", "GHOST", "STEEL", "FIRE", "WATER", "GRASS", "ELECTRIC", "PSYCHIC", "ICE", "DRAGON", "DARK", } -- .Modes, and the two-line description Pokedex_DisplayModeDescription prints -- under each one. -- `#` is the compression byte for POKé, four tiles either way, so the label -- is spelled out here the way every other Gen 2 screen in the port spells it. PokedexMenu.OPTION_MODES = { { label = "NEW POKéDEX MODE", mode = "NEW", lines = { " are listed by", "evolution type." } }, { label = "OLD POKéDEX MODE", mode = "OLD", lines = { " are listed by", "official type." } }, { label = "A to Z MODE", mode = "A-Z", lines = { " are listed", "alphabetically." } }, { label = "UNOWN MODE", mode = "UNOWN", unown = true, lines = { "UNOWN are listed", "in catching order." } }, } -- Pokedex_CheckUnlockedUnownMode: `ld a, [wStatusFlags] / bit -- STATUSFLAGS_UNOWN_DEX_F`, which is ENGINE_UNOWN_DEX -- the flag the Ruins of -- Alph researcher sets when he upgrades the #DEX, NOT "an Unown has been -- caught". The two come apart in play: the researcher only turns up once a -- puzzle has been solved, so a player who catches an Unown before talking to -- him has the mode hidden until he has upgraded the machine. function PokedexMenu:unownUnlocked() local flags = (self.save and self.save.engineFlags) or {} return flags[Unown.ENGINE_UNOWN_DEX] == true end function PokedexMenu:optionRows() local rows = {} for _, entry in ipairs(PokedexMenu.OPTION_MODES) do if not entry.unown or self:unownUnlocked() then rows[#rows + 1] = entry end end return rows end function PokedexMenu:updateOption(input) local rows = self:optionRows() if input:wasPressed("up") then self.optionIndex = self.optionIndex > 1 and self.optionIndex - 1 or #rows elseif input:wasPressed("down") then self.optionIndex = self.optionIndex < #rows and self.optionIndex + 1 or 1 elseif input:wasPressed("b") or input:wasPressed("select") then -- .return_to_main_screen: SELECT and B both back out without changing -- anything. self.view = "list" elseif input:wasPressed("a") then local row = rows[self.optionIndex] if row and row.unown then -- .MenuAction_UnownMode is the one option row that does NOT go back to -- the listing: it sets DEXSTATE_UNOWN_MODE, its own screen. self.view = "unown" self.unownIndex = 0 return end self.view = "list" if row then for index, name in ipairs(MODES) do if name == row.mode then -- .ChangeMode resets the listing to the top when the mode actually -- changes, and does nothing at all when it does not. if index ~= self.modeIndex then self.modeIndex = index self.index, self.scroll = 1, 0 self:rebuild() end end end end end end -- ------------------------------------------------------------- UNOWN MODE -- -- Pokedex_InitUnownMode / Pokedex_UpdateUnownMode (engine/pokedex/pokedex.asm) -- plus PrintUnownWord (engine/pokedex/unown_dex.asm). -- -- The screen is the FORM list, not the species list: wUnownDex holds the -- letters in the order they were first caught, wDexUnownCount is how many of -- them there are, and wDexCurUnownIndex walks that list with LEFT and RIGHT -- only. A or B leaves, back to the OPTION screen. -- -- The letters are laid out in a ring around the picture and the ring is -- addressed by SLOT, not by letter: UnownModeLetterAndCursorCoords row 0 is -- (4,11) whatever letter was caught first. function PokedexMenu:unownDex() return Unown.dex(self.save) end function PokedexMenu:updateUnown(input) local list = self:unownDex() if input:wasPressed("a") or input:wasPressed("b") then -- .a_b returns to DEXSTATE_OPTION_SCR, not to the listing. self.view = "option" return end if input:wasPressed("right") then -- `.right`: `inc a / cp e / ret nc` -- the last slot cannot advance, and -- there is no wrap. if self.unownIndex + 1 < #list then self.unownIndex = self.unownIndex + 1 end elseif input:wasPressed("left") then if self.unownIndex > 0 then self.unownIndex = self.unownIndex - 1 end end end -- UnownModeLetterAndCursorCoords, transcribed: the letter cell and the cursor -- cell for each of the 26 SLOTS, running up the left column, along the top and -- back down the right. PokedexMenu.UNOWN_COORDS = { { 4, 11, 3, 11 }, { 4, 10, 3, 10 }, { 4, 9, 3, 9 }, { 4, 8, 3, 8 }, { 4, 7, 3, 7 }, { 4, 6, 3, 6 }, { 4, 5, 3, 5 }, { 4, 4, 3, 4 }, { 4, 3, 3, 2 }, { 5, 3, 5, 2 }, { 6, 3, 6, 2 }, { 7, 3, 7, 2 }, { 8, 3, 8, 2 }, { 9, 3, 9, 2 }, { 10, 3, 10, 2 }, { 11, 3, 11, 2 }, { 12, 3, 12, 2 }, { 13, 3, 13, 2 }, { 14, 3, 15, 2 }, { 14, 4, 15, 4 }, { 14, 5, 15, 5 }, { 14, 6, 15, 6 }, { 14, 7, 15, 7 }, { 14, 8, 15, 8 }, { 14, 9, 15, 9 }, { 14, 10, 15, 10 }, } -- Pokedex_DrawUnownModeBG: FillBackgroundColor2, a 10x13 box at (2,1), a 1x13 -- box at (2,14), the two arrow tiles at (2,15) and (16,15), and the frontpic -- at (6,5). The word goes at hlcoord 4, 15, twelve cells wide. function PokedexMenu:drawUnown() local list = self:unownDex() self:fill(TILE_BG, 0, 0, Chrome.SCREEN_W, Chrome.SCREEN_H) self:border(2, 1, 10, 13) self:border(2, 14, 1, 13) self:tile(0x3d, 2, 15) self:tile(0x3e, 16, 15) local slot = math.min(self.unownIndex or 0, math.max(0, #list - 1)) local letter = list[slot + 1] if letter then -- Pokedex_LoadUnownFrontpicTiles: the pic is the FORM's, which is why -- pokemon.lua's UNOWN entry carries letters.A..Z. self:drawUnownPic(letter, 6, 5) self:unownText(Unown.word(letter) or "", 4, 15) end for index, value in ipairs(list) do local coords = PokedexMenu.UNOWN_COORDS[index] if coords then self:unownText(Unown.name(value) or "?", coords[1], coords[2]) if index == slot + 1 then self:unownCursor(coords[3], coords[4]) end end end end -- ------------------------------------------------------------- Unown font -- -- Everything UNOWN MODE prints -- the ring of letters AND the word under the -- picture -- is written in the Unown font, not in the ordinary one. The -- ring is `ld a, [hl] / add FIRST_UNOWN_CHAR - 1` (Pokedex_UpdateUnownMode), -- and data/pokemon/unown_words.asm's own `unownword` macro spells each word -- as `CHARVAL(...) - 'A' + FIRST_UNOWN_CHAR`, so a word is 26 letter tiles -- too. A cache built before UnownFont was extracted has no sheet, and both -- fall back to the inverted ordinary font, which is what this screen printed -- before. -- One letter of the Unown font: 'A' is tile FIRST_UNOWN_CHAR. Anything that -- is not a letter (the '?' a corrupt slot would print) has no tile at all and -- goes through the ordinary font instead. function PokedexMenu:unownGlyph(char, tx, ty) local sheet = self.unownFont if not (sheet and sheet:available()) then return false end local index = Unown.index(char) if not index then return false end return sheet:draw((self.unownFontBase or 0x40) + index - 1, tx, ty) end function PokedexMenu:unownText(str, tx, ty) local text = tostring(str or "") for i = 1, #text do local char = text:sub(i, i) if not self:unownGlyph(char, tx + i - 1, ty) then self:text(char, tx + i - 1, ty) end end end -- FIRST_UNOWN_CHAR + NUM_UNOWN, the diamond the ring is pointed at with: the -- 27th tile of the sheet, which is why the font is one tile longer than the -- alphabet. Without the sheet the shared cursor glyph stands in. function PokedexMenu:unownCursor(tx, ty) local sheet = self.unownFont if sheet and sheet:available() and sheet:draw((self.unownFontBase or 0x40) + Unown.NUM_UNOWN, tx, ty) then return end self:text("\xe2\x96\xb6", tx, ty) end function PokedexMenu:drawUnownPic(letter, tx, ty) local path = Unown.formSprite(self.pokemon, letter, false) if not path then return end local cached = self.picCache[path] if cached == nil then local ok, image = pcall(Assets.image, path) cached = ok and image or false self.picCache[path] = cached end if not cached then return end local colors = self.palettes and Palettes.monColors(self.palettes, Unown.SPECIES) local G = love.graphics G.setColor(1, 1, 1, 1) local function body() G.draw(cached, tx * 8, ty * 8) end if colors and GbcPalette.available() then GbcPalette.with(colors, body) else body() end end -- Pokedex_UpdateSearchScreen: four rows, and left/right walk the type wheel -- on the two that carry one. function PokedexMenu:updateSearch(input) local row = self.searchIndex if input:wasPressed("up") then self.searchIndex = row > 1 and row - 1 or 4 elseif input:wasPressed("down") then self.searchIndex = row < 4 and row + 1 or 1 elseif row <= 2 and (input:wasPressed("left") or input:wasPressed("right")) then local delta = input:wasPressed("right") and 1 or -1 local count = #PokedexMenu.SEARCH_TYPES self.searchType[row] = (self.searchType[row] + delta) % (count + 1) elseif input:wasPressed("b") or input:wasPressed("start") then self.view = "list" elseif input:wasPressed("a") then if row <= 2 then -- .MenuAction_MonSearchType: A steps the wheel too. local count = #PokedexMenu.SEARCH_TYPES self.searchType[row] = (self.searchType[row] + 1) % (count + 1) elseif row == 3 then self:beginSearch() else self.view = "list" end end end function PokedexMenu:searchTypeName(slot) local index = self.searchType[slot] or 0 if index == 0 then return "-----" end return PokedexMenu.SEARCH_TYPES[index] or "-----" end -- Pokedex_SearchForMons: a mon matches when its two types cover both of the -- wanted ones, in either order; "-----" matches anything. Only SEEN mon are -- searched, which is what makes the count meaningful. function PokedexMenu:beginSearch() local want1 = self.searchType[1] ~= 0 and self:searchTypeName(1) or nil local want2 = self.searchType[2] ~= 0 and self:searchTypeName(2) or nil local results = {} for _, entry in ipairs(self.rows) do if entry.seen then local def = self.pokemon and self.pokemon[entry.species] local types = (def and def.types) or {} local a, b = types[1], types[2] or types[1] local ok = true if want1 then ok = ok and (a == want1 or b == want1) end if want2 then ok = ok and (a == want2 or b == want2) end if ok then results[#results + 1] = entry end end end self.searchResults = results if #results == 0 then -- .MenuAction_BeginSearch redraws the search screen and stays put when -- nothing matched. self.searchMessage = "No found!" return end self.searchMessage = nil self.rows = results self.index, self.scroll = 1, 0 self:ensureVisible() self.view = "list" end -- Pokedex_DrawOptionScreenBG: two bordered boxes, the title on the top rule, -- the mode list from (3,4) two rows apart and the description at (1,14). function PokedexMenu:drawOption() self:fill(TILE_BG, 0, 0, Chrome.SCREEN_W, Chrome.SCREEN_H) self:border(0, 2, 8, 18) self:border(0, 12, 4, 18) -- `db $3b, " OPTION ", $3c`: the two end-cap tiles are the dex sheet's, not -- font glyphs. self:tile(0x3b, 0, 1) self:text(" OPTION ", 1, 1) self:tile(0x3c, 9, 1) local rows = self:optionRows() for i, row in ipairs(rows) do self:text(row.label, 3, 2 + i * 2) if i == self.optionIndex then self:text("\xe2\x96\xb6", 2, 2 + i * 2) end end local current = rows[self.optionIndex] if current then self:text(current.lines[1], 1, 14) self:text(current.lines[2], 1, 15) end end -- Pokedex_DrawSearchScreenBG: one tall box, TYPE1/TYPE2 at (3,4) and (3,6) -- with their left/right arrows at column 8, and the two-row menu at (3,13). function PokedexMenu:drawSearch() self:fill(TILE_BG, 0, 0, Chrome.SCREEN_W, Chrome.SCREEN_H) self:border(0, 2, 14, 18) self:tile(0x3b, 0, 1) self:text(" SEARCH ", 1, 1) self:tile(0x3c, 9, 1) self:text("TYPE1", 3, 4) self:text("TYPE2", 3, 6) self:text(self:searchTypeName(1), 10, 4) self:text(self:searchTypeName(2), 10, 6) -- `.TypeLeftRightArrows: db $3d, " ", $3e` -- two of the dex sheet's -- own tiles at columns 8 and 17, not font glyphs. for _, y in ipairs({ 4, 6 }) do self:tile(0x3d, 8, y) self:tile(0x3e, 17, y) end self:text("BEGIN SEARCH!!", 3, 13) self:text("CANCEL", 3, 15) if self.searchMessage then self:text(self.searchMessage, 3, 10) end local rows = { 4, 6, 13, 15 } local y = rows[self.searchIndex] or 4 self:text("\xe2\x96\xb6", 2, y) end function PokedexMenu:drawPanel() if not self:styled() then self:drawPlain() love.graphics.setColor(1, 1, 1, 1) return end local G = love.graphics -- Every dex screen starts from a full-screen fill, so the frame under it -- is the sheet's own background rather than the white a text box wants. G.setColor(0, 0, 0, 1) G.rectangle("fill", 0, 0, Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8) if self.view == "entry" then self:drawEntry() elseif self.view == "area" then self:drawArea() elseif self.view == "option" then self:drawOption() elseif self.view == "search" then self:drawSearch() elseif self.view == "unown" then self:drawUnown() else self:drawList() end G.setColor(1, 1, 1, 1) end function PokedexMenu:draw() self:drawPanel() end function PokedexMenu:drawWidescreen(winW, winH) local G = love.graphics G.setColor(0, 0, 0, 1) G.rectangle("fill", 0, 0, winW, winH) local scale = Chrome.fitScale(winW, winH) G.push() G.translate(math.floor((winW - 160 * scale) / 2), math.floor((winH - 144 * scale) / 2)) G.scale(scale, scale) self:drawPanel() G.pop() end PokedexMenu.MODES = MODES PokedexMenu.printNumString = printNumString return PokedexMenu