-- Gold's trainer card (engine/menus/trainer_card.asm), transcribed from the -- routines that draw it rather than laid out by eye. -- -- The card is not a text box. TrainerCard_InitBorder writes the frame out of -- the sheet's own tiles: a row of $23, then a row of $23 + 17 spaces + $04 + -- $23, then d rows of $23 + 18 spaces + $23, then $23 + $24 + 17 spaces + -- $23, then a closing row of $23 -- d + 4 rows in all. It is called twice, -- at (0,0) with d = 5 (rows 0-8) and at (0,8) with d = 6 (rows 8-17), so the -- two halves share row 8. -- -- Page 1 (TrainerCard_PrintTopHalfOfCard + _Page1_PrintDexCaught_GameTime): -- (2,2) "NAME/", twice to (2,6) "MONEY"; the blank middle line is -- then overwritten by the $27/$28 "ID No" tiles at (2,4) -- (7,2) player name, (5,4) ID as 5 digits with leading zeros, -- (7,6) money as PRINTNUM_MONEY 6 digits -- (1,3) the $25 x12 + $26 divider -- (14,1) the player's portrait: a 5x7 block of running tile ids from $00 -- (2,8) the $29..$2d status caption -- (2,10) "#DEX" and (2,12) "PLAY TIME"; (15,10) caught count, -- (11,12) hours as 4 digits then (16,12) minutes, with the colon at -- (15,12) blinking every 32 frames -- (12,15) "BADGES▶" -- -- Pages 2 and 3 (TrainerCard_Page2_3_InitObjectsAndStrings): the $79..$7d -- "BADGES" caption at (2,8), then eight gym leader faces from LeaderGFX -- -- four at row 10 and four at row 13, each ten tiles laid 4 across then two -- rows of 3 offset one column in -- with the badges themselves as OBJs out of -- BadgeGFX at TrainerCard_JohtoBadgesOAM's coordinates. Page 3's LoadGFX -- requests LeaderGFX2/BadgeGFX2, but those INCBIN the identical .2bpp files, -- and TrainerCard_Page3_Joypad still hands TrainerCard_Page2_3_AnimateBadges -- the Johto table -- whose header word is `dw wJohtoBadges` -- so the "Kanto" -- page is really the Johto page redrawn: same faces, same badges, gated on -- the same flags. We match that rather than wire it to wKantoBadges. -- -- Colour comes from _CGB_TrainerCard: the whole attrmap is palette 1 -- (Falkner's trainer colours, which is what tints the frame), the portrait -- box is palette 0 (the player's), and each leader's lower two rows get that -- leader's own palette. local Chrome = require("src.ui.gen2.Chrome") local GbcPalette = require("src.render.GbcPalette") local TileSheet = require("src.ui.gen2.TileSheet") local TrainerCard = {} TrainerCard.__index = TrainerCard TrainerCard.isOpaque = true local SCREEN_W, SCREEN_H = 20, 18 -- Frame tiles, which live at $23 because ChrisPicAndTrainerCardGFX is the -- 35-tile portrait followed by the 6-tile frame. local TILE_FRAME = 0x23 local TILE_NOTCH_LOW = 0x24 -- the (1, bottom-1) notch local TILE_NOTCH_HIGH = 0x04 -- the (18, top+1) one, from the portrait sheet local TILE_DIVIDER = 0x25 local TILE_DIVIDER_END = 0x26 local TILE_ID_NO = { 0x27, 0x28 } local TILE_STATUS = { 0x29, 0x2a, 0x2b, 0x2c, 0x2d } local TILE_BADGES_CAPTION = { 0x79, 0x7a, 0x7b, 0x7c, 0x7d } local TILE_COLON = 0x2e -- Johto then Kanto, in badge order. local JOHTO_BADGES = { "ZEPHYR", "HIVE", "PLAIN", "FOG", "STORM", "MINERAL", "GLACIER", "RISING", } local KANTO_BADGES = { "BOULDER", "CASCADE", "THUNDER", "RAINBOW", "SOUL", "MARSH", "VOLCANO", "EARTH", } -- TrainerCard_JohtoBadgesOAM lists the badges in wJohtoBadges bit order, -- which is not the order they are drawn in: Mineral comes before Storm. local BADGE_OAM_ORDER = { "ZEPHYR", "HIVE", "PLAIN", "FOG", "MINERAL", "STORM", "GLACIER", "RISING", } function TrainerCard:wantsFillScale() return true end function TrainerCard:drawsWidescreen() return true end -- opts: save, onClose(), sprites (sprites.lua), palettes, menuGfx function TrainerCard.new(game, opts) opts = opts or {} local self = setmetatable({}, TrainerCard) self.game = game self.save = opts.save or (game and game.save) local data = game and game.data or {} self.palettes = opts.palettes or data.gen2Palettes self.sprites = opts.sprites or data.gen2Sprites self.onClose = opts.onClose self.page = 1 -- 1 card, 2 Johto badges, 3 Kanto badges self.frames = 0 local gfx = (opts.menuGfx or data.gen2MenuGfx or {}).trainerCard self.gfx = gfx if gfx then -- One palette lookup per cell, flattened from the FillBoxCGB zones the -- way PackGfx does it. Anything outside a zone is palette 1. self.zone = {} for _, z in ipairs(gfx.paletteZones or {}) do for y = z[2], z[2] + z[4] - 1 do for x = z[1], z[1] + z[3] - 1 do self.zone[y * SCREEN_W + x] = z[5] end end end local function paletteFor(_, tx, ty) return self:colorsAt(tx, ty) end self.card = TileSheet.new({ path = gfx.card, wide = gfx.cardTilesWide or 16, firstTile = 0, paletteFor = paletteFor, }) self.status = TileSheet.new({ path = gfx.status, wide = gfx.statusWide or 6, firstTile = gfx.statusFirstTile or 0x29, paletteFor = paletteFor, }) self.leaders = TileSheet.new({ path = gfx.leaders, wide = gfx.leadersWide or 10, firstTile = gfx.leadersFirstTile or 0x29, paletteFor = paletteFor, }) self.badges = TileSheet.new({ path = gfx.badges, wide = gfx.badgesWide or 2, firstTile = 0, palette = gfx.badgePalette, }) end return self end function TrainerCard:styled() return self.card ~= nil and self.card:available() end -- The eight BG palettes _CGB_TrainerCard loads: the player's, then the seven -- leaders whose classes have a pic palette of their own. function TrainerCard:palette(index) local pals = self.palettes if not pals then return nil end if index == 1 then return self:pair(pals.trainers and pals.trainers.PLAYER) end local classes = self.gfx and self.gfx.leaderClasses or {} return self:pair(pals.trainers and pals.trainers[classes[index - 1]]) end -- LoadPalette_White_Col1_Col2_Black: the two stored colours sit between white -- and black, which is how every trainer palette in the game is used. function TrainerCard:pair(colors) if not (colors and colors[1] and colors[2]) then return nil end return { { 255, 255, 255 }, colors[1], colors[2], { 0, 0, 0 }, } end function TrainerCard:colorsAt(tx, ty) local index = (self.zone and self.zone[ty * SCREEN_W + tx]) or 2 return self:palette(index) end function TrainerCard:pages() -- Kanto's page only exists once the player has been there; the cart gates it -- on the Kanto badges having started. local kanto = self.save and self.save.player and next(self.save.player.kantoBadges or {}) ~= nil return kanto and 3 or 2 end function TrainerCard:update(_dt) self.frames = self.frames + 1 local input = self.game and self.game.input if not input then return end if input:wasPressed("b") or input:wasPressed("start") then if self.onClose then self.onClose() end return end local pages = self:pages() if input:wasPressed("right") then self.page = self.page < pages and self.page + 1 or 1 elseif input:wasPressed("left") then self.page = self.page > 1 and self.page - 1 or pages elseif input:wasPressed("a") then -- Page 1's A is "turn the page"; page 2's A quits (TrainerCard_Page2_Joypad). if self.page == 1 then self.page = 2 elseif self.onClose then self.onClose() end end end function TrainerCard:caughtCount() local caught = 0 for _, has in pairs((self.save and self.save.pokedex and self.save.pokedex.caught) or {}) do if has then caught = caught + 1 end end return caught end -- PRINTNUM_MONEY prints a ¥ in front of the first significant digit rather -- than at a fixed column, and the field is six digits wide. local function moneyText(amount) local digits = ("%06d"):format(math.max(0, math.floor(amount or 0))) local first = digits:find("[1-9]") or #digits return (" "):rep(first - 1) .. "\xc2\xa5" .. digits:sub(first) end function TrainerCard:tile(sheet, id, tx, ty) if sheet then sheet:draw(id, tx, ty) end end -- TrainerCard_InitBorder, literally. function TrainerCard:frame(ty, interiorRows) local sheet = self.card for x = 0, SCREEN_W - 1 do self:tile(sheet, TILE_FRAME, x, ty) end self:tile(sheet, TILE_FRAME, 0, ty + 1) self:tile(sheet, TILE_NOTCH_HIGH, 18, ty + 1) self:tile(sheet, TILE_FRAME, 19, ty + 1) for row = 2, interiorRows + 1 do self:tile(sheet, TILE_FRAME, 0, ty + row) self:tile(sheet, TILE_FRAME, 19, ty + row) end local notch = ty + interiorRows + 2 self:tile(sheet, TILE_FRAME, 0, notch) self:tile(sheet, TILE_NOTCH_LOW, 1, notch) self:tile(sheet, TILE_FRAME, 19, notch) for x = 0, SCREEN_W - 1 do self:tile(sheet, TILE_FRAME, x, ty + interiorRows + 3) end end -- The 5x7 portrait: running tile ids from $00, row-major, at (14,1). function TrainerCard:drawPortrait() local wide = self.gfx and self.gfx.portraitWide or 5 local high = math.floor((self.gfx and self.gfx.portraitTiles or 35) / wide) for row = 0, high - 1 do for col = 0, wide - 1 do self:tile(self.card, row * wide + col, 14 + col, 1 + row) end end end -- TrainerCard_PrintTopHalfOfCard runs once, in .InitRAM, and no page redraws -- it -- so the name, ID, money and portrait stay on screen behind the badge -- pages too. function TrainerCard:drawTopHalf() local player = (self.save or {}).player or {} self:frame(0, 5) Chrome.print("NAME/", 2, 2) Chrome.print(player.name or "GOLD", 7, 2) self:tile(self.card, TILE_ID_NO[1], 2, 4) self:tile(self.card, TILE_ID_NO[2], 3, 4) Chrome.print(Chrome.number(player.id or 0, 5, true), 5, 4) Chrome.print("MONEY", 2, 6) Chrome.print(moneyText(player.money), 7, 6) for x = 1, 12 do self:tile(self.card, TILE_DIVIDER, x, 3) end self:tile(self.card, TILE_DIVIDER_END, 13, 3) self:drawPortrait() end function TrainerCard:drawCard() local save = self.save or {} self:drawTopHalf() self:frame(8, 6) -- The $29..$2d caption plaque, which sits on the row the two halves share. for i, id in ipairs(TILE_STATUS) do self:tile(self.status, id, 1 + i, 8) end -- `#` is the compression byte for POKé, four tiles, so spelling it out is -- what the cart actually draws. Chrome.print("POKéDEX", 2, 10) Chrome.print("PLAY TIME", 2, 12) Chrome.print(Chrome.number(self:caughtCount(), 3), 15, 10) local time = save.playTime or {} Chrome.print(Chrome.number(time.hours or 0, 4), 11, 12) -- The colon is $2e, which belongs to CardStatusGFX rather than the card -- sheet, and TrainerCard_Page1_PrintGameTime xors it with ' ' every 32 -- frames -- which is what makes the clock look like it is running. if math.floor(self.frames / 32) % 2 == 0 then self:tile(self.status, TILE_COLON, 15, 12) end Chrome.print(Chrome.number(time.minutes or 0, 2, true), 16, 12) Chrome.print("BADGES", 12, 15) Chrome.cursor(18, 15) end -- TrainerCard_Page2_3_PlaceLeadersFaces: four tiles across the top row, then -- two rows of three offset one column in, ten tiles per face with the id -- running on across all eight. function TrainerCard:drawLeaderFace(first, tx, ty) local id = first for col = 0, 3 do self:tile(self.leaders, id, tx + col, ty) id = id + 1 end for row = 1, 2 do for col = 1, 3 do self:tile(self.leaders, id, tx + col, ty + row) id = id + 1 end end return id end function TrainerCard:drawBadgeSprites(owned, names) local list = self.gfx and self.gfx.badgeOam if not (list and self.badges and self.badges:available()) then return end local G = love.graphics -- Eight frames on a 3-bit counter, stepped every 8 VBlanks. local frame = math.floor(self.frames / 8) % 8 for i, obj in ipairs(list) do -- The OAM table is in wJohtoBadges bit order, which is not the drawing -- order (Mineral is listed before Storm), so the earned flag is looked up -- by name -- or by that name's position, since a save may key the array -- either way. local name = BADGE_OAM_ORDER[i] local slot for index, badge in ipairs(names) do if badge == name then slot = index end end if owned[name] or (slot and owned[slot]) then local tile = obj.frames[frame + 1] or 0 -- Bit 7 of the tile id is an x-flip, which is how Risingbadge turns. local flip = tile >= 0x80 local base = flip and (tile - 0x80) or tile local sx = flip and -1 or 1 local function body() for _, cell in ipairs({ { 0, 0, 0 }, { 1, 0, 1 }, { 0, 1, 2 }, { 1, 1, 3 } }) do local quad = self.badges:quad(base + cell[3]) if quad then local px = obj.x + (flip and (1 - cell[1]) or cell[1]) * 8 G.draw(self.badges:image(), quad, px + (flip and 8 or 0), obj.y + cell[2] * 8, 0, sx, 1) end end end G.setColor(1, 1, 1, 1) if self.gfx.badgePalette and GbcPalette.available() then GbcPalette.with(self.gfx.badgePalette, body) else body() end end end end function TrainerCard:drawBadges(names, owned) self:drawTopHalf() self:frame(8, 6) for i, id in ipairs(TILE_BADGES_CAPTION) do self:tile(self.leaders, id, 1 + i, 8) end local id = self.gfx and self.gfx.leadersFirstTile or 0x29 for face = 0, 3 do id = self:drawLeaderFace(id, 2 + face * 4, 10) end for face = 0, 3 do id = self:drawLeaderFace(id, 2 + face * 4, 13) end self:drawBadgeSprites(owned, names) end -- ------------------------------------------------------------------ fallback function TrainerCard:drawPlain() local save = self.save or {} local player = save.player or {} Chrome.clear() if self.page == 1 then Chrome.box(0, 0, 20, 9) Chrome.print("NAME/", 2, 2) Chrome.print(player.name or "GOLD", 7, 2) Chrome.print("ID No", 2, 4) Chrome.print(Chrome.number(player.id or 0, 5, true), 5, 4) Chrome.print("MONEY", 2, 6) Chrome.print(moneyText(player.money), 7, 6) Chrome.box(0, 8, 20, 10) Chrome.print("POKéDEX", 2, 10) Chrome.print(Chrome.number(self:caughtCount(), 3), 15, 10) Chrome.print("PLAY TIME", 2, 12) local time = save.playTime or {} Chrome.print(Chrome.number(time.hours or 0, 4), 11, 12) Chrome.print(":", 15, 12) Chrome.print(Chrome.number(time.minutes or 0, 2, true), 16, 12) Chrome.print("BADGES", 12, 15) Chrome.cursor(18, 15) return end local names = self.page == 2 and JOHTO_BADGES or KANTO_BADGES -- TrainerCard_Page3_Joypad hands TrainerCard_Page2_3_AnimateBadges the -- exact same TrainerCard_JohtoBadgesOAM pointer page 2 uses, and that -- table's own header word is `dw wJohtoBadges` -- so page 3 never once -- reads wKantoBadges, it just relabels the Johto flags. held stays -- player.badges on both pages to match. local held = player.badges or {} Chrome.box(0, 0, 20, 9) Chrome.print(self.page == 2 and "JOHTO BADGES" or "KANTO BADGES", 2, 2) Chrome.box(0, 8, 20, 10) for i, name in ipairs(names) do local tx = 2 + ((i - 1) % 4) * 4 local ty = 10 + math.floor((i - 1) / 4) * 3 Chrome.print((held[i] or held[name]) and name:sub(1, 4) or "----", tx, ty) end end function TrainerCard:drawPanel() if not self:styled() then self:drawPlain() love.graphics.setColor(1, 1, 1, 1) return end local G = love.graphics G.setColor(1, 1, 1, 1) G.rectangle("fill", 0, 0, SCREEN_W * 8, SCREEN_H * 8) local player = (self.save and self.save.player) or {} if self.page == 1 then self:drawCard() elseif self.page == 2 then self:drawBadges(JOHTO_BADGES, player.badges or {}) else -- Page 3 reuses TrainerCard_JohtoBadgesOAM wholesale, header word and -- all, so it is really the Johto page again: JOHTO_BADGES here keeps -- drawBadgeSprites' name/position fallback matched to player.badges, -- the same table page 2 reads. self:drawBadges(JOHTO_BADGES, player.badges or {}) end G.setColor(1, 1, 1, 1) end function TrainerCard:draw() self:drawPanel() end function TrainerCard:drawWidescreen(winW, winH) local G = love.graphics G.setColor(1, 1, 1, 1) G.rectangle("fill", 0, 0, winW, winH) local scale = Chrome.fitScale(winW, winH) G.push() G.translate(Chrome.fitOrigin(winW, winH, scale)) G.scale(scale, scale) self:drawPanel() G.pop() end TrainerCard.JOHTO_BADGES = JOHTO_BADGES TrainerCard.KANTO_BADGES = KANTO_BADGES TrainerCard.BADGE_OAM_ORDER = BADGE_OAM_ORDER TrainerCard.moneyText = moneyText return TrainerCard