Files
gen1recomp/src/ui/gen2/PokedexMenu.lua
T

1465 lines
54 KiB
Lua

-- 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 <NEXT> 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 "") .. "<NEXT>"):gmatch("(.-)<NEXT>") 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 "") .. "<NEXT>"):gmatch("(.-)<NEXT>") 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 = { "<PK><MN> are listed by", "evolution type." } },
{ label = "OLD POKéDEX MODE", mode = "OLD",
lines = { "<PK><MN> are listed by", "official type." } },
{ label = "A to Z MODE", mode = "A-Z",
lines = { "<PK><MN> 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 <PK><MN> 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