mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
1140 lines
43 KiB
Lua
1140 lines
43 KiB
Lua
-- Gold's mon SUMMARY, transcribed from engine/pokemon/stats_screen.asm.
|
|
--
|
|
-- Three pages, named after the palette each one wears rather than after what
|
|
-- it shows: PINK_PAGE (1), GREEN_PAGE (2), BLUE_PAGE (3). It is worth saying
|
|
-- out loud which is which, because Gen 1's stats screen split the same
|
|
-- information differently and the temptation is to lay this out from memory:
|
|
--
|
|
-- PINK HP bar, HP digits, STATUS/TYPE, and the EXP POINTS / LEVEL UP TO
|
|
-- block down the right of a vertical rule at column 9
|
|
-- GREEN the held ITEM, then the four moves with their PP
|
|
-- BLUE OT / <ID>№ down the left of a vertical rule at column 10, and the
|
|
-- five non-HP stats down its right
|
|
--
|
|
-- None of the three is a text box. StatsScreenMain calls ClearTilemap and
|
|
-- then every routine writes tiles at its own hlcoord, so every coordinate in
|
|
-- this file is that hlcoord and nothing here is placed by eye.
|
|
--
|
|
-- StatsScreen_InitUpperHalf draws rows 0-7 once and no page redraws them, so
|
|
-- the pic, dex number, nickname, level, gender, species and the row-7 rule
|
|
-- stay put while the lower half changes. That is why `upperPlacements` is
|
|
-- separate from the per-page ones and why switching pages does not replay the
|
|
-- cry: LoadPinkPage only jumps to StatsScreen_PlaceFrontpic (which calls
|
|
-- PlayMonCry) when b is 0, and b is 0 only on the entry from StatsScreenMain
|
|
-- -- a page switch comes back through .done_loading with `ld b, 1`.
|
|
--
|
|
-- Tiles that are not glyphs
|
|
-- -------------------------
|
|
-- StatsScreen_LoadFont (engine/gfx/load_font.asm) is _LoadFontsBattleExtra
|
|
-- plus ExpBarGFX at $55, and LoadStatsScreenPageTilesGFX puts the 17 tiles of
|
|
-- gfx/stats/stats_tiles.png at $31:
|
|
--
|
|
-- $31 the vertical divider column
|
|
-- $36-$39 the small (inactive) page square, 2x2
|
|
-- $3a-$3d the large (active) page square, 2x2
|
|
-- $3e the "P" of the PP label
|
|
-- $3f the shiny ⁂ icon (stats_tiles tile 14)
|
|
-- $40 / $41 the left and right HP/exp bar end caps
|
|
--
|
|
-- The extractor does not carry that sheet yet, so `pageTile` draws those seven
|
|
-- shapes directly and takes the sheet the moment menu_gfx grows a `stats`
|
|
-- entry. Everything that IS a glyph goes through the font: ◀ ($71), ▶ ($ed),
|
|
-- № ($74), <ID> ($73), <LV> ($6e) and the row-7 rule's $62 (the empty HP/exp
|
|
-- bar cell, which is FontBattleExtra's -- hence Font.useBattleExtra(true)
|
|
-- around the whole screen, exactly as the party menu does).
|
|
--
|
|
-- Move descriptions
|
|
-- -----------------
|
|
-- The three stats pages have no room for one: LoadGreenPage's ClearBox is
|
|
-- rows 8-17 and the move names and PP already fill every one of them. Where
|
|
-- the cart shows a move description is PlaceMoveData (engine/pokemon/
|
|
-- mon_menu.asm), the screen MoveScreenLoop puts up, and that whole screen is
|
|
-- transcribed here as `moveDetailPlacements`. SELECT opens it from the green
|
|
-- page; SELECT is not in the stats screen's accepted-button mask
|
|
-- (PAD_CTRL_PAD | PAD_A | PAD_B), so borrowing it leaves every button the ASM
|
|
-- does handle behaving exactly as it does.
|
|
--
|
|
-- ManagePokemonMoves (engine/pokemon/mon_menu.asm:858) opens that same screen
|
|
-- on its own, which is what the party submenu's MOVE row does: `moveScreen`
|
|
-- says the move list IS the screen, so B exits instead of dropping onto a page.
|
|
|
|
local Assets = require("src.render.Assets")
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local Font = require("src.render.Font")
|
|
local GbcPalette = require("src.render.GbcPalette")
|
|
local HpBar = require("src.battle.gen2.HpBar")
|
|
local ItemEffects = require("src.core.gen2.ItemEffects")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local Palettes = require("src.world.gen2.Palettes")
|
|
local Pokerus = require("src.core.gen2.Pokerus")
|
|
local Unown = require("src.core.gen2.Unown")
|
|
|
|
local SummaryMenu = {}
|
|
SummaryMenu.__index = SummaryMenu
|
|
SummaryMenu.isOpaque = true
|
|
|
|
-- The *_PAGE constants at the top of stats_screen.asm.
|
|
SummaryMenu.PINK_PAGE = 1
|
|
SummaryMenu.GREEN_PAGE = 2
|
|
SummaryMenu.BLUE_PAGE = 3
|
|
SummaryMenu.NUM_STAT_PAGES = 3
|
|
|
|
local PINK_PAGE = SummaryMenu.PINK_PAGE
|
|
local GREEN_PAGE = SummaryMenu.GREEN_PAGE
|
|
local BLUE_PAGE = SummaryMenu.BLUE_PAGE
|
|
|
|
-- Tile ids from StatsScreenPageTilesGFX, kept named so `pageTile` reads like
|
|
-- the ASM that asks for them.
|
|
local TILE_VERTICAL_DIVIDER = 0x31
|
|
local TILE_SQUARE_SMALL = 0x36
|
|
local TILE_SQUARE_LARGE = 0x3a
|
|
local TILE_SHINY = 0x3f
|
|
local TILE_BAR_CAP_LEFT = 0x40
|
|
local TILE_BAR_CAP_RIGHT = 0x41
|
|
-- FontBattleExtra's empty HP/exp bar cell, which is what the row-7 rule is
|
|
-- made of (StatsScreen_PlaceHorizontalDivider).
|
|
local TILE_HORIZONTAL_DIVIDER = 0x62
|
|
|
|
-- PrintTempMonStats' .StatNames, and the wTempMon fields it prints beside
|
|
-- them. <NEXT> steps two rows, so the five labels are 2 rows apart and the
|
|
-- values start one row below the first label.
|
|
local STAT_LABELS = { "ATTACK", "DEFENSE", "SPCL.ATK", "SPCL.DEF", "SPEED" }
|
|
local STAT_KEYS = {
|
|
"attack", "defense", "specialAttack", "specialDefense", "speed",
|
|
}
|
|
|
|
-- data/types/names.asm. Every type constant prints as its own name except
|
|
-- the two the extractor has to disambiguate against Lua-unfriendly ids.
|
|
local TYPE_NAMES = {
|
|
PSYCHIC_TYPE = "PSYCHIC",
|
|
CURSE_TYPE = "???",
|
|
}
|
|
|
|
-- Gen 2 pics are 5x5, 6x6 or 7x7 and PadFrontpic centres the small ones in
|
|
-- the 7x7 block PrepMonFrontpic lays at hlcoord 0, 0. Same table the dex
|
|
-- uses, for the same reason.
|
|
local PIC_PAD = { [7] = { 0, 0 }, [6] = { 1, 1 }, [5] = { 1, 2 } }
|
|
|
|
-- Matches src/core/gen2/Breeding.lua's isEgg without requiring it, the same
|
|
-- arms-length test src/core/gen2/Happiness.lua carries.
|
|
local function isEggMon(mon)
|
|
return type(mon) == "table" and mon.isEgg == true
|
|
end
|
|
|
|
-- EggStatsScreen's four flavour strings, picked off wTempMonHappiness --
|
|
-- which on an egg is not happiness at all but the remaining hatch cycles
|
|
-- DoEggStep counts down (256 steps each, `eggSteps` here). The thresholds
|
|
-- are the ASM's own `cp $6 / cp $b / cp $29` ladder, and the lines join with
|
|
-- <NEXT> exactly as the db/next strings do.
|
|
local EGG_FLAVOR = {
|
|
{ below = 0x6, text = "It's making sounds<NEXT>inside. It's going"
|
|
.. "<NEXT>to hatch soon!" },
|
|
{ below = 0xb, text = "It moves around<NEXT>inside sometimes."
|
|
.. "<NEXT>It must be close<NEXT>to hatching." },
|
|
{ below = 0x29, text = "Wonder what's<NEXT>inside? It needs"
|
|
.. "<NEXT>more time, though." },
|
|
{ text = "This EGG needs a<NEXT>lot more time to<NEXT>hatch." },
|
|
}
|
|
|
|
local function eggFlavor(cycles)
|
|
for _, entry in ipairs(EGG_FLAVOR) do
|
|
if not entry.below or cycles < entry.below then return entry.text end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------- placements
|
|
--
|
|
-- Every page is built as a list of { text, x, y } tilemap writes before
|
|
-- anything is drawn, so the layout can be asserted without a graphics device
|
|
-- (tests/gen2_summary_test.lua) and `drawPanel` stays a loop.
|
|
|
|
local function put(list, text, x, y)
|
|
if text == nil then return list end
|
|
list[#list + 1] = { text = tostring(text), x = x, y = y }
|
|
return list
|
|
end
|
|
|
|
-- The text a placement list writes at a coordinate, or nil.
|
|
function SummaryMenu.at(placements, x, y)
|
|
for _, entry in ipairs(placements or {}) do
|
|
if entry.x == x and entry.y == y then return entry.text end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- PrintNum: a right-aligned field, space-padded unless PRINTNUM_LEADINGZEROS.
|
|
local function num(value, width, leadingZeros)
|
|
return Chrome.number(value, width, leadingZeros)
|
|
end
|
|
|
|
-- How many tiles a string occupies, which is what PlaceString advances by --
|
|
-- not its pixel width. "<PK>" and "é" are one tile each, and Font.split is
|
|
-- the only thing in the port that knows that.
|
|
local function tiles(text)
|
|
return #Font.split(tostring(text or ""))
|
|
end
|
|
|
|
-- GetNickname reads wPartyMonNicknames, so a party mon always has a name of
|
|
-- its own; a directly-built mon may only carry a species.
|
|
local function monName(mon)
|
|
return mon.nickname or mon.name or mon.species or "?"
|
|
end
|
|
|
|
-- Blank whole tile cells. PlaceString writes tilemap cells, so a string put
|
|
-- over a box border REPLACES it; here the border is pixels and a glyph sheet
|
|
-- is transparent, so without this the frame runs straight through the letters
|
|
-- of any string the ASM places on a border row.
|
|
local function clearCells(tx, ty, tw, th)
|
|
local G = love.graphics
|
|
G.setColor(1, 1, 1, 1)
|
|
G.rectangle("fill", tx * 8, ty * 8, tw * 8, (th or 1) * 8)
|
|
G.setColor(0, 0, 0, 1)
|
|
end
|
|
|
|
-- PrintLevel (home/pokemon.asm): '<LV>' then the number left-aligned in two
|
|
-- cells. A three-digit level does `dec hl` first, so the digits overwrite the
|
|
-- <LV> and the field starts at the same column either way.
|
|
local function levelText(level)
|
|
level = math.max(1, math.floor(tonumber(level) or 1))
|
|
if level >= 100 then return tostring(level) end
|
|
return "<LV>" .. tostring(level)
|
|
end
|
|
|
|
-- PlaceStatusString (engine/pokemon/mon_stats.asm): three letters, and a mon
|
|
-- with no HP reads FNT whatever its status byte says. Same lookup the party
|
|
-- list makes; both screens call the same routine on the cart.
|
|
local function statusText(mon)
|
|
if (mon.hp or 0) <= 0 then return "FNT" end
|
|
local status = mon.status
|
|
if not status then return nil end
|
|
local class = ItemEffects.STATUS_CLASS[tostring(status):lower()]
|
|
return class and class:upper()
|
|
end
|
|
|
|
-- wTempMonPokerusStatus is one byte: the low nibble counts the days left and
|
|
-- the high nibble holds the strain. A cured mon keeps a non-zero high nibble
|
|
-- forever, and that is what the '.' at (8,8) marks -- so "infected" and
|
|
-- "immune" really are two different tests of the same byte, in that order.
|
|
local function pokerusState(mon)
|
|
if Pokerus.isInfected(mon) then return "infected" end
|
|
if Pokerus.isImmune(mon) then return "immune" end
|
|
return nil
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ screen
|
|
|
|
function SummaryMenu:wantsFillScale() return true end
|
|
function SummaryMenu:drawsWidescreen() return true end
|
|
|
|
-- opts: mon, party, index, page, moveScreen, onClose(), save, pokemon, moves,
|
|
-- items, palettes, menuGfx
|
|
function SummaryMenu.new(game, opts)
|
|
opts = opts or {}
|
|
local self = setmetatable({}, SummaryMenu)
|
|
self.game = game
|
|
local data = (game and game.data) or {}
|
|
self.save = opts.save or (game and game.save)
|
|
self.pokemon = opts.pokemon or data.pokemon
|
|
self.moves = opts.moves or data.moves
|
|
self.items = opts.items or data.items
|
|
self.palettes = opts.palettes or data.gen2Palettes
|
|
-- The egg page's pic does not live in data.pokemon; it comes off the same
|
|
-- menu_gfx.eggHatch entry the hatch cutscene draws.
|
|
self.menuGfx = opts.menuGfx or data.gen2MenuGfx
|
|
-- ...and icons.lua carries ICON_EGG, which is what stands in when a cache
|
|
-- was built before the extractor learned EggPic. See drawEggPic.
|
|
self.icons = opts.icons or data.gen2Icons
|
|
self.onClose = opts.onClose
|
|
if opts.mon and not opts.party then
|
|
-- One mon on its own: the wMonType == TEMPMON / BOXMON path, where the
|
|
-- `down` and `.d_up` arms have nothing to scroll to.
|
|
self.party = { opts.mon }
|
|
self.index = 1
|
|
self.mon = opts.mon
|
|
else
|
|
self.party = opts.party or (self.save and self.save.party) or {}
|
|
self.index = math.max(1,
|
|
math.min(opts.index or 1, math.max(1, #self.party)))
|
|
self.mon = self.party[self.index]
|
|
end
|
|
-- engine/pokemon/move_mon.asm:1402
|
|
Mon.refreshStats(self.mon, data)
|
|
self.page = opts.page or PINK_PAGE
|
|
-- ManagePokemonMoves opens straight onto MoveScreenLoop's screen; SELECT off
|
|
-- the green page reaches the same view with the stats pages still behind it.
|
|
self.moveScreen = opts.moveScreen == true
|
|
self.moveDetail = self.moveScreen
|
|
self.moveIndex = 1
|
|
-- wSwappingMove: the row A picked a move up from, nil while nothing is held.
|
|
self.swapFrom = nil
|
|
self.picCache = {}
|
|
|
|
-- The HP and exp bars are the battle HUD's, tile for tile: DrawPlayerHP is
|
|
-- DrawBattleHPBar and the exp bar is FillInExpBar, so both go through
|
|
-- BattleHud rather than being drawn a second time here.
|
|
local BattleHud = require("src.ui.gen2.BattleHud")
|
|
self.hud = BattleHud.new(data.gen2MenuGfx, self.palettes)
|
|
self:playCry()
|
|
return self
|
|
end
|
|
|
|
-- StatsScreen_PlaceFrontpic ends in PlayMonCry, and it only runs when b is 0
|
|
-- -- the entry from StatsScreenMain. So the cry fires on open and on a mon
|
|
-- switch, never on a page switch.
|
|
function SummaryMenu:playCry()
|
|
local mon = self.mon
|
|
-- SetUpMoveScreenBG only loads the menu icon (engine/pokemon/mon_menu.asm:
|
|
-- 1106-1107); MoveScreenLoop has no PlayMonCry at all, on entry or on cycle.
|
|
if self.moveScreen then return end
|
|
-- StatsScreenInit routes an EGG to EggStatsInit before any pic or cry, so
|
|
-- an egg never plays the hidden species' voice. EggStatsScreen ends on its
|
|
-- own cue instead (engine/pokemon/stats_screen.asm:788): `ld a,
|
|
-- [wTempMonHappiness] / cp 6 / ret nc / ld de, SFX_2_BOOPS`, i.e. the boops
|
|
-- sound only once the egg is nearly ready, pairing with the "It's making
|
|
-- sounds inside" line. The threshold is the same $6 EGG_FLAVOR's first row
|
|
-- reads off eggSteps, so the two cannot drift apart.
|
|
if isEggMon(mon) then
|
|
if (mon.eggSteps or 0) >= 0x6 then return end
|
|
if not (self.game and self.game.data) then return end
|
|
local ok, Sound = pcall(require, "src.core.Sound")
|
|
if ok and Sound and Sound.play then
|
|
pcall(Sound.play, self.game.data, "Sfx_2Boops")
|
|
end
|
|
return
|
|
end
|
|
if not (mon and mon.species and self.game and self.game.data) then return end
|
|
local ok, Sound = pcall(require, "src.core.Sound")
|
|
if ok and Sound and Sound.playCry then
|
|
pcall(Sound.playCry, self.game.data, mon.species)
|
|
end
|
|
end
|
|
|
|
function SummaryMenu:speciesDef()
|
|
local mon = self.mon
|
|
return mon and self.pokemon and self.pokemon[mon.species] or nil
|
|
end
|
|
|
|
function SummaryMenu:moveDef(id)
|
|
return id and self.moves and self.moves[id] or nil
|
|
end
|
|
|
|
-- The mon's four move slots, empty ones left as holes so the '-' rows land on
|
|
-- the right lines.
|
|
function SummaryMenu:moveList()
|
|
return (self.mon and self.mon.moves) or {}
|
|
end
|
|
|
|
function SummaryMenu:moveName(entry)
|
|
if not entry then return nil end
|
|
local def = self:moveDef(entry.id)
|
|
return (def and def.name) or entry.id
|
|
end
|
|
|
|
-- The EXP page's "to next level" gap reads the same curve the battle does, so
|
|
-- it goes through Mon.growthFor. self.pokemon IS data.pokemon, so a synthetic
|
|
-- data table with just that key is what the accessor needs.
|
|
function SummaryMenu:growth()
|
|
local def = self:speciesDef()
|
|
if not (self.pokemon and def) then return nil end
|
|
local data = (self.game and self.game.data) or { pokemon = self.pokemon }
|
|
return require("src.battle.gen2.Mon").growthFor(data, def.growthRate)
|
|
end
|
|
|
|
-- .CalcExpToNextLevel: zero at MAX_LEVEL, otherwise the gap to the next
|
|
-- level's threshold.
|
|
function SummaryMenu:expToNext()
|
|
local mon = self.mon or {}
|
|
local level = mon.level or 1
|
|
if level >= Mon.MAX_LEVEL then return 0 end
|
|
return math.max(0, Mon.experienceForLevel(self:growth(), level + 1)
|
|
- (mon.experience or 0))
|
|
end
|
|
|
|
function SummaryMenu:otName()
|
|
local mon = self.mon or {}
|
|
if mon.otName then return mon.otName end
|
|
local player = (self.save and self.save.player) or {}
|
|
return player.name or "GOLD"
|
|
end
|
|
|
|
function SummaryMenu:otId()
|
|
local mon = self.mon or {}
|
|
if mon.otId then return mon.otId end
|
|
local player = (self.save and self.save.player) or {}
|
|
return player.id or 0
|
|
end
|
|
|
|
-- .PlaceOTInfo's closing block: `lb bc, 0, -1` counts the characters up to the
|
|
-- '@', then `ld a, NAME_LENGTH - 1; sub c` and, unless that came out under
|
|
-- NAME_LENGTH - PLAYER_NAME_LENGTH (3), clamps to
|
|
-- NAME_LENGTH - PLAYER_NAME_LENGTH - 1 (2). So an ordinary name gets two
|
|
-- spaces of left padding and only a 9 or 10 character one is pulled left.
|
|
function SummaryMenu.otColumn(name)
|
|
local pad = 10 - tiles(name)
|
|
if pad >= 3 or pad < 0 then pad = 2 end
|
|
return pad
|
|
end
|
|
|
|
function SummaryMenu:itemName()
|
|
local mon = self.mon or {}
|
|
if not mon.item then return nil end
|
|
local def = self.items and self.items[mon.item]
|
|
return (def and def.name) or mon.item
|
|
end
|
|
|
|
function SummaryMenu:typeNames()
|
|
local def = self:speciesDef()
|
|
local types = (self.mon and self.mon.types) or (def and def.types) or {}
|
|
local first = types[1]
|
|
local second = types[2] or first
|
|
local function name(id)
|
|
if not id then return nil end
|
|
return TYPE_NAMES[id] or id
|
|
end
|
|
-- PrintMonTypes' .hide_type_2: a single-typed mon really has two of the same
|
|
-- type, and the second name is blanked rather than printed twice.
|
|
if first and second and first == second then return name(first), nil end
|
|
return name(first), name(second)
|
|
end
|
|
|
|
-- ------------------------------------------------------------ upper half
|
|
|
|
-- StatsScreen_InitUpperHalf, coordinate for coordinate.
|
|
function SummaryMenu:upperPlacements()
|
|
local mon = self.mon or {}
|
|
local def = self:speciesDef()
|
|
local out = {}
|
|
-- (8,0) '№' and (9,0) '.' are two `ld [hl]` writes, then PrintNum puts the
|
|
-- dex number in three leading-zero digits at (10,0).
|
|
put(out, "№.", 8, 0)
|
|
put(out, num(def and def.dex or 0, 3, true), 10, 0)
|
|
put(out, levelText(mon.level), 14, 0)
|
|
put(out, mon.nickname or mon.name or mon.species, 8, 2)
|
|
-- GetGender returns carry for a genderless species, and nothing is written.
|
|
if mon.gender == "male" then
|
|
put(out, "♂", 18, 0)
|
|
elseif mon.gender == "female" then
|
|
put(out, "♀", 18, 0)
|
|
end
|
|
-- (9,4) is a bare '/' written with `ld [hli], a`, so the species name that
|
|
-- follows starts at (10,4).
|
|
put(out, "/", 9, 4)
|
|
put(out, (def and def.name) or mon.species, 10, 4)
|
|
return out
|
|
end
|
|
|
|
-- ------------------------------------------------------------- pink page
|
|
|
|
function SummaryMenu:pinkPlacements()
|
|
local mon = self.mon or {}
|
|
local maxHp = mon.maxHp or (mon.stats and mon.stats.hp) or 0
|
|
local out = {}
|
|
|
|
-- DrawPlayerHP puts the bar at (0,9) and then, from that same hl,
|
|
-- `bccoord 1, 1, 0` steps to (1,10) for the digits: current HP in three
|
|
-- columns, the '/' it writes with `ld [hli]`, then max HP in three more.
|
|
put(out, num(mon.hp, 3), 1, 10)
|
|
put(out, "/", 4, 10)
|
|
put(out, num(maxHp, 3), 5, 10)
|
|
|
|
-- .Status_Type is "STATUS/" <NEXT> "TYPE/", and <NEXT> is two rows down at
|
|
-- the same column -- so the second label is at row 14, not row 13.
|
|
put(out, "STATUS/", 0, 12)
|
|
put(out, "TYPE/", 0, 14)
|
|
|
|
local pokerus = pokerusState(mon)
|
|
if pokerus == "infected" then
|
|
-- .PkrsStr is "#RUS", and '#' is the four-tile POKé compression byte.
|
|
put(out, "POKéRUS", 1, 13)
|
|
else
|
|
if pokerus == "immune" then put(out, ".", 8, 8) end
|
|
put(out, statusText(mon) or "OK", 6, 13)
|
|
end
|
|
|
|
-- PrintMonTypes writes type 1 at (1,15) and type 2 two rows below it, and
|
|
-- then LoadPinkPage copies the nine bytes of row 17 up onto row 16 and
|
|
-- blanks row 17 -- so the second type ends up one row under the first.
|
|
local type1, type2 = self:typeNames()
|
|
put(out, type1, 1, 15)
|
|
put(out, type2, 1, 16)
|
|
|
|
put(out, "EXP POINTS", 10, 9)
|
|
-- `lb bc, 3, 7`: a three-byte value in seven columns, so the field runs
|
|
-- (13,10) to (19,10).
|
|
put(out, num(mon.experience, 7), 13, 10)
|
|
put(out, "LEVEL UP", 10, 12)
|
|
put(out, num(self:expToNext(), 7), 13, 13)
|
|
put(out, "TO", 14, 14)
|
|
-- The level printed at (17,14) is the NEXT one: LoadPinkPage bumps
|
|
-- wTempMonLevel, calls PrintLevel, and puts it back. MAX_LEVEL stays put.
|
|
local level = mon.level or 1
|
|
put(out, levelText(math.min(Mon.MAX_LEVEL, level + 1)), 17, 14)
|
|
return out
|
|
end
|
|
|
|
-- ------------------------------------------------------------ green page
|
|
|
|
function SummaryMenu:greenPlacements()
|
|
local out = {}
|
|
put(out, "ITEM", 0, 8)
|
|
put(out, self:itemName() or "---", 6, 8)
|
|
put(out, "MOVE", 0, 10)
|
|
|
|
-- ListMoves runs from (8,10) with wListMovesLineSpacing = SCREEN_WIDTH * 2,
|
|
-- so the four names are two rows apart; ListMovePP runs from (12,11) with
|
|
-- the same spacing, one row below each name. An empty slot gets a single
|
|
-- '-' for the name and the PP label's two cells get '-' as well.
|
|
local moves = self:moveList()
|
|
for slot = 1, 4 do
|
|
local nameY = 10 + (slot - 1) * 2
|
|
local ppY = nameY + 1
|
|
local entry = moves[slot]
|
|
if entry then
|
|
put(out, self:moveName(entry), 8, nameY)
|
|
-- Two $3e "P" tiles: `ld [hli], a` then `ld [hld], a` writes the same
|
|
-- tile at (12,y) and (13,y).
|
|
put(out, "PP", 12, ppY)
|
|
-- `pop hl` then three `inc hl` lands the numbers at (15,y): two digits,
|
|
-- the '/' PrintNum's caller writes, then two more.
|
|
put(out, num(entry.pp, 2), 15, ppY)
|
|
put(out, "/", 17, ppY)
|
|
put(out, num(entry.maxPp or entry.pp, 2), 18, ppY)
|
|
else
|
|
put(out, "-", 8, nameY)
|
|
put(out, "--", 12, ppY)
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- ------------------------------------------------------------- blue page
|
|
|
|
function SummaryMenu:bluePlacements()
|
|
local mon = self.mon or {}
|
|
local out = {}
|
|
-- IDNoString is "<ID>№." -- three single tiles, not the seven letters of
|
|
-- "ID No." -- and OTString is "OT/".
|
|
put(out, "<ID>№.", 0, 9)
|
|
put(out, num(self:otId(), 5, true), 2, 10)
|
|
put(out, "OT/", 0, 12)
|
|
local ot = self:otName()
|
|
put(out, ot, SummaryMenu.otColumn(ot), 13)
|
|
|
|
-- PrintTempMonStats is called at (11,8) with bc = 6: the labels go in at hl
|
|
-- two rows apart, then `add hl, bc` and one more SCREEN_WIDTH puts the first
|
|
-- value at (17,9) -- three columns wide, so every value ends at column 19.
|
|
for i, label in ipairs(STAT_LABELS) do
|
|
put(out, label, 11, 8 + (i - 1) * 2)
|
|
local value = (mon.stats or {})[STAT_KEYS[i]]
|
|
put(out, num(value, 3), 17, 9 + (i - 1) * 2)
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- ------------------------------------------------------- move detail view
|
|
--
|
|
-- SetUpMoveScreenBG + SetUpMoveList + PlaceMoveData (engine/pokemon/
|
|
-- mon_menu.asm). Two text boxes: Textbox (0,1) with a 9x18 interior, so rows
|
|
-- 1-11, and Textbox (0,11) with a 5x18 interior, so rows 11-17 -- the two
|
|
-- share row 11, which is why the TYPE plaque's bottom line sits on it.
|
|
|
|
function SummaryMenu:moveDetailPlacements()
|
|
local mon = self.mon or {}
|
|
local out = {}
|
|
local name = monName(mon)
|
|
put(out, name, 5, 1)
|
|
-- PlaceString leaves bc one tile past the string and MoveScreenLoop pops
|
|
-- that straight into hl for PrintLevel, so the level butts up against the
|
|
-- nickname rather than sitting in a column.
|
|
put(out, levelText(mon.level), 5 + tiles(name), 1)
|
|
|
|
local moves = self:moveList()
|
|
for slot = 1, 4 do
|
|
local nameY = 3 + (slot - 1) * 2
|
|
local ppY = nameY + 1
|
|
local entry = moves[slot]
|
|
if entry then
|
|
put(out, self:moveName(entry), 2, nameY)
|
|
put(out, "PP", 10, ppY)
|
|
put(out, num(entry.pp, 2), 13, ppY)
|
|
put(out, "/", 15, ppY)
|
|
put(out, num(entry.maxPp or entry.pp, 2), 16, ppY)
|
|
else
|
|
put(out, "-", 2, nameY)
|
|
put(out, "--", 10, ppY)
|
|
end
|
|
end
|
|
|
|
-- .moving_move: five spaces over "TYPE/" at (1,11), ClearBox (1,12) 5x18,
|
|
-- then String_MoveWhere at (1,12) -- so the data half reads "Where?" alone.
|
|
if self.swapFrom then
|
|
put(out, "┌─────┐", 0, 10)
|
|
put(out, "│", 0, 11)
|
|
put(out, "└", 6, 11)
|
|
put(out, "Where?", 1, 12)
|
|
return out
|
|
end
|
|
|
|
-- String_MoveType_Top / _Bottom are box-drawing glyphs, and the plaque is
|
|
-- open on its right: "┌─────┐" over "│TYPE/└".
|
|
put(out, "┌─────┐", 0, 10)
|
|
put(out, "│TYPE/└", 0, 11)
|
|
put(out, "ATTK/", 11, 12)
|
|
|
|
local entry = moves[self.moveIndex]
|
|
local def = entry and self:moveDef(entry.id)
|
|
local moveType = def and def.type
|
|
put(out, moveType and (TYPE_NAMES[moveType] or moveType) or "---", 2, 12)
|
|
-- `cp 2; jr c, .no_power`: a move with power 0 or 1 prints String_MoveNoPower
|
|
-- rather than a number.
|
|
local power = (def and def.power) or 0
|
|
if power >= 2 then
|
|
put(out, num(power, 3), 16, 12)
|
|
else
|
|
put(out, "---", 16, 12)
|
|
end
|
|
|
|
-- PrintMoveDescription at (1,14). Descriptions join their lines with
|
|
-- <NEXT>, which is two rows down at the same column, so the second line is
|
|
-- at row 16 and not row 15.
|
|
local description = def and def.description or ""
|
|
local ty = 14
|
|
for line in (tostring(description) .. "<NEXT>"):gmatch("(.-)<NEXT>") do
|
|
if ty > 16 then break end
|
|
if line ~= "" then put(out, line, 1, ty) end
|
|
ty = ty + 2
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- --------------------------------------------------------------- egg page
|
|
--
|
|
-- EggStatsScreen (engine/pokemon/stats_screen.asm). One screen, no pages,
|
|
-- no arrows, no gender glyph: "EGG" where the nickname block sits, five
|
|
-- question marks for both the ID and the OT, and a flavour line that reads
|
|
-- the hatch counter. Everything else about the egg -- species, level,
|
|
-- stats, moves -- stays a secret.
|
|
|
|
function SummaryMenu:eggPlacements()
|
|
local mon = self.mon or {}
|
|
local out = {}
|
|
put(out, "EGG", 8, 1)
|
|
-- IDNoString / OTString, the same strings the blue page prints, with
|
|
-- FiveQMarkString beside each: an egg's OT and ID are hidden.
|
|
put(out, "<ID>№.", 8, 3)
|
|
put(out, "?????", 11, 3)
|
|
put(out, "OT/", 8, 5)
|
|
put(out, "?????", 11, 5)
|
|
local ty = 9
|
|
for line in ((eggFlavor(mon.eggSteps or 0) or "") .. "<NEXT>")
|
|
:gmatch("(.-)<NEXT>") do
|
|
if line ~= "" then put(out, line, 1, ty) end
|
|
ty = ty + 2
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Everything the current view writes, upper half included. The move detail
|
|
-- clears the tilemap for itself (SetUpMoveScreenBG), so it does not carry it.
|
|
function SummaryMenu:placements()
|
|
if isEggMon(self.mon) then return self:eggPlacements() end
|
|
if self.moveDetail then return self:moveDetailPlacements() end
|
|
local out = self:upperPlacements()
|
|
local page
|
|
if self.page == GREEN_PAGE then
|
|
page = self:greenPlacements()
|
|
elseif self.page == BLUE_PAGE then
|
|
page = self:bluePlacements()
|
|
else
|
|
page = self:pinkPlacements()
|
|
end
|
|
for _, entry in ipairs(page) do out[#out + 1] = entry end
|
|
return out
|
|
end
|
|
|
|
-- ------------------------------------------------------------------- input
|
|
|
|
function SummaryMenu:close()
|
|
if self.onClose then self.onClose() end
|
|
end
|
|
|
|
-- .d_right / .d_left. Right adds one and wraps past BLUE_PAGE back to
|
|
-- PINK_PAGE; left subtracts one and wraps from zero to BLUE_PAGE.
|
|
function SummaryMenu:turnPage(delta)
|
|
local page = self.page + delta
|
|
if page > BLUE_PAGE then page = PINK_PAGE end
|
|
if page < PINK_PAGE then page = BLUE_PAGE end
|
|
self.page = page
|
|
end
|
|
|
|
-- The `down` and `.d_up` arms: no wrap at either end (both `jr z, .joypad_loop`
|
|
-- out rather than rolling round), and the page survives the switch because
|
|
-- StatsScreenMain pushes bc on the way in and pops it before jumping to the
|
|
-- page loader.
|
|
function SummaryMenu:switchMon(delta)
|
|
local next_ = self.index + delta
|
|
if next_ < 1 or next_ > #self.party then return false end
|
|
self.index = next_
|
|
self.mon = self.party[next_]
|
|
-- engine/pokemon/move_mon.asm:1402
|
|
Mon.refreshStats(self.mon, self.game and self.game.data)
|
|
self.moveIndex = 1
|
|
self:playCry()
|
|
return true
|
|
end
|
|
|
|
-- MoveScreenLoop's .cycle_right_loop / .cycle_left_loop: the move screen
|
|
-- walks the party but steps over EGG slots (`cp EGG / ret nz` inverted), so
|
|
-- it never opens on one. No wrap at either end, like switchMon.
|
|
function SummaryMenu:switchMonPastEggs(delta)
|
|
local next_ = self.index + delta
|
|
while self.party[next_] and isEggMon(self.party[next_]) do
|
|
next_ = next_ + delta
|
|
end
|
|
if next_ < 1 or next_ > #self.party then return false end
|
|
self.index = next_
|
|
self.mon = self.party[next_]
|
|
-- engine/pokemon/move_mon.asm:1402
|
|
Mon.refreshStats(self.mon, self.game and self.game.data)
|
|
self.moveIndex = 1
|
|
self:playCry()
|
|
return true
|
|
end
|
|
|
|
-- .place_move's `.copy_move` pair swaps the move byte and then the PP byte, so
|
|
-- a slot travels whole -- id, PP and the PP-Up ceiling ride in one entry here.
|
|
function SummaryMenu:swapMoves(from, to)
|
|
local moves = self.mon and self.mon.moves
|
|
if not (moves and from and to) or from == to then return false end
|
|
if not (moves[from] and moves[to]) then return false end
|
|
moves[from], moves[to] = moves[to], moves[from]
|
|
return true
|
|
end
|
|
|
|
-- .swap_moves plays SFX_SWITCH_POKEMON twice (mon_menu.asm:1036-1041); a cache
|
|
-- without that cue simply makes no sound, the way the box menu guards its own.
|
|
function SummaryMenu:playSwapSfx()
|
|
local data = self.game and self.game.data
|
|
local ok, Sound = pcall(require, "src.core.Sound")
|
|
if not (ok and data and Sound and Sound.play) then return end
|
|
local sfx = data.audio and data.audio.sfx
|
|
if sfx and sfx[Sound.resolve(data, "Sfx_SwitchPokemon")] then
|
|
pcall(Sound.play, data, "Sfx_SwitchPokemon")
|
|
end
|
|
end
|
|
|
|
-- MoveScreenLoop's .joy_loop. A picks a move up (.a_button stores wMenuCursorY
|
|
-- in wSwappingMove and draws the hollow cursor) and puts it down (.place_move);
|
|
-- B drops it back on the row it came from and only then exits.
|
|
function SummaryMenu:updateMoveDetail(input)
|
|
local moves = self:moveList()
|
|
local count = math.max(1, #moves)
|
|
if input:wasPressed("up") then
|
|
self.moveIndex = self.moveIndex > 1 and self.moveIndex - 1 or count
|
|
elseif input:wasPressed("down") then
|
|
self.moveIndex = self.moveIndex < count and self.moveIndex + 1 or 1
|
|
elseif input:wasPressed("a") then
|
|
if self.swapFrom then
|
|
if self:swapMoves(self.swapFrom, self.moveIndex) then self:playSwapSfx() end
|
|
self.swapFrom = nil
|
|
elseif moves[self.moveIndex] then
|
|
self.swapFrom = self.moveIndex
|
|
end
|
|
elseif input:wasPressed("right") then
|
|
-- MoveScreenLoop's .d_right / .d_left walk the party rather than the page,
|
|
-- and both `jp nz, .joy_loop` straight back out while a move is held.
|
|
if not self.swapFrom then self:switchMonPastEggs(1) end
|
|
elseif input:wasPressed("left") then
|
|
if not self.swapFrom then self:switchMonPastEggs(-1) end
|
|
elseif input:wasPressed("b") or input:wasPressed("select") then
|
|
if self.swapFrom then
|
|
self.moveIndex = self.swapFrom
|
|
self.swapFrom = nil
|
|
elseif self.moveScreen then
|
|
-- .exit: ManagePokemonMoves' whole screen goes, back to the party list.
|
|
self:close()
|
|
else
|
|
self.moveDetail = false
|
|
end
|
|
end
|
|
end
|
|
|
|
function SummaryMenu:update(_dt)
|
|
local input = self.game and self.game.input
|
|
if not input then return end
|
|
if self.moveDetail then
|
|
self:updateMoveDetail(input)
|
|
return
|
|
end
|
|
|
|
-- EggStats_JoypadLoop masks PAD_DOWN | PAD_UP | PAD_A | PAD_B: A and B
|
|
-- both exit (StatsScreen_Exit), up and down walk the party, and there are
|
|
-- no pages to turn.
|
|
if isEggMon(self.mon) then
|
|
if input:wasPressed("a") or input:wasPressed("b") then
|
|
self:close()
|
|
elseif input:wasPressed("up") then
|
|
self:switchMon(-1)
|
|
elseif input:wasPressed("down") then
|
|
self:switchMon(1)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- .joypad_action masks with PAD_CTRL_PAD | PAD_A | PAD_B and tests B first.
|
|
if input:wasPressed("b") then
|
|
self:close()
|
|
return
|
|
end
|
|
if input:wasPressed("left") then
|
|
self:turnPage(-1)
|
|
return
|
|
end
|
|
if input:wasPressed("right") then
|
|
self:turnPage(1)
|
|
return
|
|
end
|
|
if input:wasPressed("a") then
|
|
-- .a_button quits on the last page and otherwise FALLS THROUGH into
|
|
-- .d_right; the fallthrough is the whole behaviour, not a missing branch.
|
|
if self.page == BLUE_PAGE then
|
|
self:close()
|
|
else
|
|
self:turnPage(1)
|
|
end
|
|
return
|
|
end
|
|
if input:wasPressed("up") then
|
|
self:switchMon(-1)
|
|
return
|
|
end
|
|
if input:wasPressed("down") then
|
|
self:switchMon(1)
|
|
return
|
|
end
|
|
-- The port's own hook onto MoveScreenLoop; see the header.
|
|
if input:wasPressed("select") and self.page == GREEN_PAGE then
|
|
self.moveDetail = true
|
|
self.moveIndex = 1
|
|
end
|
|
end
|
|
|
|
-- ----------------------------------------------------------------- drawing
|
|
|
|
-- A tile out of StatsScreenPageTilesGFX. The extractor does not carry that
|
|
-- sheet, so each of the seven shapes it needs is drawn here; the moment
|
|
-- menu_gfx grows a `stats` entry this can take the real tiles instead.
|
|
function SummaryMenu:pageTile(id, tx, ty)
|
|
local G = love.graphics
|
|
local px, py = tx * 8, ty * 8
|
|
G.setColor(0, 0, 0, 1)
|
|
if id == TILE_VERTICAL_DIVIDER then
|
|
G.rectangle("fill", px + 3, py, 2, 8)
|
|
elseif id == TILE_BAR_CAP_LEFT then
|
|
G.rectangle("fill", px + 6, py + 1, 2, 6)
|
|
elseif id == TILE_BAR_CAP_RIGHT then
|
|
G.rectangle("fill", px, py + 1, 2, 6)
|
|
elseif id == TILE_SHINY then
|
|
-- ⁂ is an asterism: three dots, two low and one high.
|
|
G.rectangle("fill", px + 2, py + 1, 2, 2)
|
|
G.rectangle("fill", px, py + 4, 2, 2)
|
|
G.rectangle("fill", px + 4, py + 4, 2, 2)
|
|
end
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
-- StatsScreen_LoadPageIndicators: three 2x2 squares at (13,5), (15,5) and
|
|
-- (17,5), all small ($36) first, then the one for this page redrawn large
|
|
-- ($3a). The routine writes the four tiles as [hli]/[hld], a row down, then
|
|
-- [hli]/[hl] -- which is why it is a 2x2 block and not a 2x1 strip.
|
|
function SummaryMenu:drawPageSquare(tx, ty, large)
|
|
local G = love.graphics
|
|
local px, py = tx * 8, ty * 8
|
|
-- $3a..$3d for the page that is up, $36..$39 for the other two.
|
|
local first = large and TILE_SQUARE_LARGE or TILE_SQUARE_SMALL
|
|
local inset = first == TILE_SQUARE_LARGE and 2 or 5
|
|
local size = 16 - inset * 2
|
|
G.setColor(0, 0, 0, 1)
|
|
G.rectangle("fill", px + inset, py + inset, size, size)
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function SummaryMenu:drawPageIndicators()
|
|
local columns = { 13, 15, 17 }
|
|
for i, tx in ipairs(columns) do
|
|
self:drawPageSquare(tx, 5, i == self.page)
|
|
end
|
|
end
|
|
|
|
function SummaryMenu:picImage(path)
|
|
if not path then return nil end
|
|
local cached = self.picCache[path]
|
|
if cached == nil then
|
|
-- `and` truncates a multi-return, so the pcall has to stand alone.
|
|
local ok, image = pcall(Assets.image, path)
|
|
cached = (ok and image) or false
|
|
self.picCache[path] = cached
|
|
end
|
|
return cached or nil
|
|
end
|
|
|
|
function SummaryMenu:picFor(mon)
|
|
local def = mon and self.pokemon and self.pokemon[mon.species]
|
|
local path = def and def.spriteFront
|
|
-- StatsScreen_PlaceFrontpic (engine/pokemon/stats_screen.asm:722): `ld hl,
|
|
-- wTempMonDVs / call GetUnownLetter` runs before the frontpic, so a party
|
|
-- Unown's page shows its own form, not letter A.
|
|
if mon and mon.species == Unown.SPECIES then
|
|
path = Unown.formSprite(self.pokemon, Unown.monLetter(mon)) or path
|
|
end
|
|
return self:picImage(path)
|
|
end
|
|
|
|
-- PrepMonFrontpic at hlcoord 0, 0: a 7x7 block with the pic padded into it and
|
|
-- the rest of the block left at the palette's colour 0.
|
|
function SummaryMenu:drawPicBlock(image, colors)
|
|
if not image then return end
|
|
local G = love.graphics
|
|
-- A fill behind the pic reads a palette colour directly, so it has to come
|
|
-- through GbcPalette.color rather than off the raw table.
|
|
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", 0, 0, 7 * 8, 7 * 8)
|
|
|
|
local wide = math.floor(image:getWidth() / 8)
|
|
local pad = PIC_PAD[wide] or PIC_PAD[7]
|
|
G.setColor(1, 1, 1, 1)
|
|
local function body() G.draw(image, pad[1] * 8, pad[2] * 8) end
|
|
if colors and GbcPalette.available() then
|
|
GbcPalette.with(colors, body)
|
|
else
|
|
body()
|
|
end
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function SummaryMenu:drawPic()
|
|
local mon = self.mon
|
|
local image = mon and self:picFor(mon)
|
|
if not image then return end
|
|
local colors = self.palettes and mon.species
|
|
and Palettes.monColors(self.palettes, mon.species, mon.shiny) or nil
|
|
self:drawPicBlock(image, colors)
|
|
end
|
|
|
|
-- EggStatsScreen ends on `hlcoord 0, 0 / call PrepMonFrontpic` as well
|
|
-- (engine/pokemon/stats_screen.asm:786), and for an egg wCurPartySpecies is
|
|
-- EGG, so GetBaseData's `.egg` arm hands GetFrontpic EggPic at 5x5
|
|
-- (home/pokemon.asm:239-245) rather than the hidden hatchling's pic. That
|
|
-- pic has no data.pokemon row of its own: the extractor writes it once as
|
|
-- menu_gfx.eggHatch.egg, the same file src/ui/gen2/EggHatchAnim.lua draws.
|
|
-- _CGB_StatsScreenHPPals colours it off the EGG palette row for the same
|
|
-- reason (`ld a, [wCurPartySpecies] / call GetPlayerOrMonPalettePointer`,
|
|
-- engine/gfx/cgb_layouts.asm:177-180), shininess read from the DVs, so a
|
|
-- shiny hatchling's egg takes EGG's shiny row.
|
|
--
|
|
-- A cache built before the extractor grew menu_gfx.eggHatch.egg has no such
|
|
-- file, and every cache imported before that stage is one -- which is the
|
|
-- whole of "the SUMMARY shows no egg picture": the 7x7 block was simply left
|
|
-- blank. ICON_EGG is in icons.lua all the way back (ReadMonMenuIcon's
|
|
-- `cp EGG / jr z, .egg` arm, engine/gfx/mon_icons.asm), so the party list's
|
|
-- own egg stands in at 2x rather than the page showing nothing. It is a
|
|
-- fallback, not the layout: a cache with EggPic in it never reaches this.
|
|
function SummaryMenu:drawEggPic()
|
|
local gfx = (self.menuGfx or {}).eggHatch
|
|
local colors = Palettes.monColors(self.palettes, "EGG",
|
|
self.mon and self.mon.shiny)
|
|
local image = self:picImage(gfx and gfx.egg)
|
|
if image then return self:drawPicBlock(image, colors) end
|
|
self:drawEggIconFallback(colors)
|
|
end
|
|
|
|
-- The ICON_EGG sheet is two 16x16 frames stacked into one 16x32 image
|
|
-- (src/ui/gen2/PartyMenu.lua reads the same entry); the first frame is the
|
|
-- egg at rest, which is the one the party list shows while nothing is moving.
|
|
function SummaryMenu:drawEggIconFallback(colors)
|
|
local icons = self.icons or {}
|
|
local entry = icons.icons and icons.icons.ICON_EGG
|
|
local image = self:picImage(entry and entry.image)
|
|
if not image then return end
|
|
local G = love.graphics
|
|
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", 0, 0, 7 * 8, 7 * 8)
|
|
local w = entry.width or 16
|
|
local h = math.min(entry.height or 16, image:getHeight())
|
|
if (entry.frames or 1) > 1 then h = math.floor(h / entry.frames) end
|
|
local ok, quad = pcall(love.graphics.newQuad, 0, 0, w, h,
|
|
image:getWidth(), image:getHeight())
|
|
if not ok then return end
|
|
-- Centred in the block at 2x: a 16x16 icon inside 7x7 tiles.
|
|
local x = math.floor((7 * 8 - w * 2) / 2)
|
|
local y = math.floor((7 * 8 - h * 2) / 2)
|
|
G.setColor(1, 1, 1, 1)
|
|
local function body() G.draw(image, quad, x, y, 0, 2, 2) end
|
|
if colors and GbcPalette.available() then
|
|
GbcPalette.with(colors, body)
|
|
else
|
|
body()
|
|
end
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function SummaryMenu:drawPlacements(list)
|
|
for _, entry in ipairs(list) do
|
|
Chrome.print(entry.text, entry.x, entry.y)
|
|
end
|
|
end
|
|
|
|
-- StatsScreen_PlaceHorizontalDivider: twenty $62 cells across row 7. $62 is
|
|
-- FontBattleExtra's empty HP/exp bar cell, so it resolves through the font
|
|
-- while useBattleExtra is on.
|
|
function SummaryMenu:drawHorizontalDivider()
|
|
love.graphics.setColor(0, 0, 0, 1)
|
|
for x = 0, Chrome.SCREEN_W - 1 do
|
|
Font.drawCode(TILE_HORIZONTAL_DIVIDER, x * 8, 7 * 8)
|
|
end
|
|
end
|
|
|
|
function SummaryMenu:drawVerticalDivider(tx)
|
|
for y = 8, 17 do self:pageTile(TILE_VERTICAL_DIVIDER, tx, y) end
|
|
end
|
|
|
|
function SummaryMenu:drawUpperHalf()
|
|
local mon = self.mon or {}
|
|
self:drawPic()
|
|
self:drawPlacements(self:upperPlacements())
|
|
self:drawHorizontalDivider()
|
|
-- StatsScreen_PlacePageSwitchArrows, then StatsScreen_PlaceShinyIcon.
|
|
Chrome.print("◀", 12, 6)
|
|
Chrome.print("▶", 19, 6)
|
|
if mon.shiny then self:pageTile(TILE_SHINY, 19, 0) end
|
|
self:drawPageIndicators()
|
|
end
|
|
|
|
function SummaryMenu:drawPinkPage()
|
|
local mon = self.mon or {}
|
|
local maxHp = mon.maxHp or (mon.stats and mon.stats.hp) or 0
|
|
-- DrawPlayerHP is DrawBattleHPBar with d = 6 and b = 0: "HP:" at (0,9), six
|
|
-- bar cells, and the end cap at (8,9) -- which LoadPinkPage then rewrites as
|
|
-- $41, the same shape from the stats sheet.
|
|
if self.hud and self.hud:available() then
|
|
self.hud:drawHpBar(mon.hp, maxHp, 0, 9)
|
|
else
|
|
HpBar.drawWithLabel(self.palettes, mon.hp, maxHp, 0, 9, Font)
|
|
end
|
|
self:drawVerticalDivider(9)
|
|
self:drawPlacements(self:pinkPlacements())
|
|
|
|
-- FillInExpBar is handed (11,16), adds 7 to reach the rightmost cell and
|
|
-- fills eight of them walking left, with the $40/$41 caps outside at (10,16)
|
|
-- and (19,16).
|
|
local fraction = HpBar.expFraction(mon, self:growth(), Mon.experienceForLevel)
|
|
if self.hud and self.hud:available() then
|
|
self.hud:drawExpBar(fraction, 11, 16)
|
|
else
|
|
-- No HUD sheet in the cache: the plain rule, which is HP_BAR_LENGTH_PX
|
|
-- (48) wide rather than the exp bar's 64, so it stops two tiles short of
|
|
-- the $41 cap. A cache old enough to hit this has no bar tiles at all.
|
|
HpBar.drawExp(self.palettes, fraction, 11 * 8, 16 * 8 + 3)
|
|
end
|
|
self:pageTile(TILE_BAR_CAP_LEFT, 10, 16)
|
|
self:pageTile(TILE_BAR_CAP_RIGHT, 19, 16)
|
|
end
|
|
|
|
function SummaryMenu:drawGreenPage()
|
|
self:drawPlacements(self:greenPlacements())
|
|
end
|
|
|
|
function SummaryMenu:drawBluePage()
|
|
self:drawVerticalDivider(10)
|
|
self:drawPlacements(self:bluePlacements())
|
|
end
|
|
|
|
function SummaryMenu:drawMoveDetail()
|
|
local mon = self.mon or {}
|
|
Chrome.clear()
|
|
-- Textbox (0,1) with a 9x18 interior and Textbox (0,11) with a 5x18 one.
|
|
Chrome.textbox(0, 1, 18, 9)
|
|
Chrome.textbox(0, 11, 18, 5)
|
|
-- SetUpMoveScreenBG draws both boxes and only THEN places the nickname at
|
|
-- (5,1) -- the upper box's own top border row -- and PlaceMoveData writes
|
|
-- the TYPE plaque at (0,10)/(0,11), the lower box's top border row. Those
|
|
-- cells belong to the strings, not to the frames.
|
|
clearCells(5, 1, tiles(monName(mon)) + tiles(levelText(mon.level)), 1)
|
|
clearCells(0, 10, 7, 2)
|
|
-- PlaceMoveScreenLeftArrow / RightArrow only draw when there is a party mon
|
|
-- that way; both sit on row 0, above the list box.
|
|
if self.index > 1 then Chrome.print("◀", 16, 0) end
|
|
if self.index < #self.party then Chrome.print("▶", 18, 0) end
|
|
self:drawPlacements(self:moveDetailPlacements())
|
|
-- MoveScreen2DMenuData: cursor column 1, first row 3, two rows per step.
|
|
local moves = self:moveList()
|
|
if #moves > 0 then
|
|
-- .a_button's PlaceHollowCursor parks a hollow '▷' on the held row.
|
|
if self.swapFrom then
|
|
Chrome.cursor(1, 3 + (math.min(self.swapFrom, #moves) - 1) * 2, true)
|
|
end
|
|
Chrome.cursor(1, 3 + (math.min(self.moveIndex, #moves) - 1) * 2)
|
|
end
|
|
end
|
|
|
|
-- EggStatsScreen's draw: the row-7 divider, the fixed strings, and
|
|
-- PrepMonFrontpic over the EGG pic (engine/pokemon/stats_screen.asm:786).
|
|
-- The pic comes from menu_gfx.eggHatch.egg, never from the hidden species'
|
|
-- battle/front entry, which is why it goes through drawEggPic.
|
|
function SummaryMenu:drawEggPage()
|
|
Chrome.clear()
|
|
self:drawEggPic()
|
|
self:drawHorizontalDivider()
|
|
self:drawPlacements(self:eggPlacements())
|
|
end
|
|
|
|
function SummaryMenu:drawPanel()
|
|
-- StatsScreen_LoadFont is _LoadFontsBattleExtra, so $60-$7f on this screen
|
|
-- is the battle sheet: <LV> is the bold ":L" and $62 is the bar cell the
|
|
-- row-7 rule is made of.
|
|
local wasBattle = Font.useBattleExtra(true)
|
|
if isEggMon(self.mon) then
|
|
self:drawEggPage()
|
|
elseif self.moveDetail then
|
|
self:drawMoveDetail()
|
|
else
|
|
Chrome.clear()
|
|
self:drawUpperHalf()
|
|
if self.page == GREEN_PAGE then
|
|
self:drawGreenPage()
|
|
elseif self.page == BLUE_PAGE then
|
|
self:drawBluePage()
|
|
else
|
|
self:drawPinkPage()
|
|
end
|
|
end
|
|
Font.useBattleExtra(wasBattle)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function SummaryMenu:draw()
|
|
self:drawPanel()
|
|
end
|
|
|
|
function SummaryMenu: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(math.floor((winW - 160 * scale) / 2),
|
|
math.floor((winH - 144 * scale) / 2))
|
|
G.scale(scale, scale)
|
|
self:drawPanel()
|
|
G.pop()
|
|
end
|
|
|
|
SummaryMenu.STAT_LABELS = STAT_LABELS
|
|
SummaryMenu.STAT_KEYS = STAT_KEYS
|
|
SummaryMenu.TYPE_NAMES = TYPE_NAMES
|
|
SummaryMenu.levelText = levelText
|
|
|
|
return SummaryMenu
|