CLOSES #1474, CLOSES #1475, CLOSES #1477, CLOSES #1482, CLOSES #1512, CLOSES #1516, CLOSES #1558, CLOSES #1569, CLOSES #1578, CLOSES #1579

This commit is contained in:
bryanthaboi
2026-08-20 16:04:12 -04:00
parent 83463a5a59
commit 0f8f6d0e4f
51 changed files with 2331 additions and 205 deletions
+108 -14
View File
@@ -38,6 +38,9 @@ local Sound = require("src.core.Sound")
-- Only for playerPic: the player.sprite raiser both generations share.
local Sprites = require("src.pokemon.Sprites")
local Strings = require("src.core.Strings")
-- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text
-- carries into the battle box (PrintWinLossText, home/trainers.asm:230).
local TextBox = require("src.render.TextBox")
local Unown = require("src.core.gen2.Unown")
local BattleState = {}
@@ -87,6 +90,20 @@ local TRAINER_SLIDE_STEPS = 8
local TRAINER_SLIDE_FRAMES_PER_STEP = 2
local TRAINER_SLIDE_FRAMES = TRAINER_SLIDE_STEPS * TRAINER_SLIDE_FRAMES_PER_STEP
-- BattleWinSlideInEnemyTrainerFrontpic (engine/battle/core.asm:6279-6318) and
-- WinTrainerBattle's DelayFrames 40 (:2311)
local WIN_SLIDE_STEPS = 6
local WIN_SLIDE_FRAMES_PER_STEP = 4
local WIN_SLIDE_FRAMES = WIN_SLIDE_STEPS * WIN_SLIDE_FRAMES_PER_STEP
local WIN_SLIDE_REST_TILES = 2
local WIN_SLIDE_DELAY_FRAMES = 40
local function winSlideTiles(frames)
local step = math.min(WIN_SLIDE_STEPS,
math.floor(frames / WIN_SLIDE_FRAMES_PER_STEP) + 1)
return WIN_SLIDE_STEPS + WIN_SLIDE_REST_TILES - step
end
-- MonFaintedAnimation (engine/battle/core.asm), which PlayerMonFaintedAnimation
-- and EnemyMonFaintedAnimation both fall into with the fainted side's pic
-- corner: the pic's tilemap rows are copied DOWN one row per step and the row
@@ -141,6 +158,10 @@ local MENU_COL_SPACING = 6
-- the count PrintNum writes after it (two digits, leading zeros) at (13,16).
local CONTEST_MENU_BOX_X = 2
local CONTEST_MENU_COL_SPACING = 12
-- PrintMoveType prints the type table's own names; only these two differ from
-- the constant (data/types/names.asm).
local TYPE_NAMES = { PSYCHIC_TYPE = "PSYCHIC", CURSE_TYPE = "???" }
-- charmap.asm's quantity glyph, spelled the way MartMenu spells it.
local CONTEST_BALL_LABEL = "PARKBALL\xc3\x97"
@@ -508,6 +529,14 @@ function BattleState:pushAll(events)
for _, event in ipairs(events or {}) do self:push(event) end
end
-- LearnMove returns before HandleEnemyMonFaint's send-out/prize arms
-- (engine/battle/core.asm:1959-2010).
function BattleState:pushFront(events)
for i = #(events or {}), 1, -1 do
table.insert(self.queue, 1, events[i])
end
end
function BattleState:pic(mon, back)
local def = self.pokemon and mon and self.pokemon[mon.species]
local path = def and (back and def.spriteBack or def.spriteFront)
@@ -686,6 +715,8 @@ function BattleState:drawPic(mon, back)
-- One tile per two frames to the right, SlideBattlePicOut's own step.
if enemyTrainer and self.trainerSlide then
px = px + math.floor(self.trainerSlide / TRAINER_SLIDE_FRAMES_PER_STEP) * 8
elseif enemyTrainer and self.winSlide then
px = px + winSlideTiles(self.winSlide) * 8
end
-- The pic's own scale (battle_sprite_scales, then the species record, then
-- 1x) composed with whatever square BattleBGEffect_RunPicResizeScript has
@@ -1371,7 +1402,7 @@ function BattleState:advanceQueue()
and self.shownHp[event.side] ~= event.hp then
self.hpAnim = { side = event.side, to = event.hp }
elseif event.kind == "send" and event.side and event.mon and self.shownHp then
self.shownHp[event.side] = event.mon.hp or 0
self.shownHp[event.side] = event.hp or event.mon.hp or 0
if self.hpAnim and self.hpAnim.side == event.side then self.hpAnim = nil end
end
-- And the same lag for the status tag (home/battle.asm:150); a send snaps it
@@ -1379,8 +1410,9 @@ function BattleState:advanceQueue()
if self.shownStatus and event.side
and (event.kind == "status"
or (event.kind == "send" and event.mon)) then
self.shownStatus[event.side] =
(event.kind == "send" and event.mon.status or event.status) or false
local shown = event.status
if event.kind == "send" and shown == nil then shown = event.mon.status end
self.shownStatus[event.side] = shown or false
end
-- AnimateExpBar (engine/battle/core.asm:7191) is called from INSIDE
-- GiveExperiencePoints before the exp is committed (the call at :6888 sits
@@ -1435,6 +1467,24 @@ function BattleState:advanceQueue()
self.trainerSlide = 0
return
end
-- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it
-- (engine/battle/core.asm:2310-2312)
if event.kind == "trainer-return" then
if not self.enemyTrainerImage then return self:advanceQueue() end
self.showEnemyTrainer = true
self.picHidden.enemy = false
self.winSlide = 0
self.winSliding = true
return
end
-- PrintWinLossText (home/trainers.asm:230): one FarPrintText of the trainer
-- struct's own line, paged and held for A/B like any other map text.
if event.kind == "win-text" then
local text = event.text
if self.game then text = TextBox.substitute(self.game, text) end
self:showPages(text)
return
end
-- The shiny sparkle: hBattleTurn 1 and wBattleAnimParam 1 pick
-- BattleAnim_SendOutMon's `.Shiny` arm on the enemy (core.asm:8708-8715).
if event.kind == "shiny-flash" then
@@ -1917,6 +1967,17 @@ function BattleState:update(_dt)
return
end
-- BattleWinSlideInEnemyTrainerFrontpic plus WinTrainerBattle's DelayFrames
-- 40 (engine/battle/core.asm:6279-6318, :2310-2312)
if self.winSliding then
self.winSlide = self.winSlide + 1
if self.winSlide >= WIN_SLIDE_FRAMES + WIN_SLIDE_DELAY_FRAMES then
self.winSliding = nil
self:advanceQueue()
end
return
end
-- SlideBattlePicOut is a plain loop with DelayFrames in it, so it owns the
-- screen the same way (engine/battle/core.asm:2882).
if self.trainerSlide then
@@ -2004,6 +2065,8 @@ function BattleState:update(_dt)
self.phase = "stats-box"
return
end
-- PrintWinLossText's line pages like any map text (home/text.asm:403-448)
if self:nextPage() then return end
self:advanceQueue()
return
end
@@ -2292,7 +2355,7 @@ function BattleState:update(_dt)
learn.move, learn.moveName)
self.pendingLearn = nil
self.phase = "resolving"
self:pushAll(self.battle:takeEvents())
self:pushFront(self.battle:takeEvents())
self:advanceQueue()
end
return
@@ -2872,7 +2935,7 @@ function BattleState:finishDecline()
self.pendingLearn = nil
self.phase = "resolving"
if learn then self.battle:declineForget(learn.index, learn.moveName) end
self:pushAll(self.battle:takeEvents())
self:pushFront(self.battle:takeEvents())
self:advanceQueue()
end
@@ -3475,6 +3538,23 @@ function BattleState:printMessage()
end
end
-- MoveInfoBox (engine/battle/core.asm:5403-5478): "TYPE/" at (1,9), the type
-- at (2,10), cur/max PP at (5,11), or "Disabled!" at (1,10).
function BattleState:drawMoveInfoBox(move)
if not move then return end
local fighter = self.battle and self.battle.player
if fighter and self.battle:moveDisabled(fighter, move.id) then
Chrome.print("Disabled!", 1, 10)
return
end
local def = self.game and self.game.data and self.game.data.moves
and self.game.data.moves[move.id]
Chrome.print("TYPE/", 1, 9)
local moveType = def and def.type
Chrome.print(moveType and (TYPE_NAMES[moveType] or moveType) or "", 2, 10)
Chrome.print(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0), 5, 11)
end
function BattleState:drawPanel()
Chrome.clear()
-- A tutorial battle legitimately has no player mon, so only the enemy is
@@ -3494,7 +3574,15 @@ function BattleState:drawPanel()
-- Message box across the bottom, with the menu window over its right half --
-- the cart draws the prompt into the full-width box and then opens the menu
-- on top, so the tail of a long name is simply covered.
Chrome.box(0, 12, 20, 6)
-- MoveSelectionScreen type 0 is two boxes: the name-only list
-- (engine/battle/core.asm:5074-5084) and MoveInfoBox's (:5407-5410).
local moveMenu = self.phase == "moves"
if moveMenu then
Chrome.box(0, 8, 11, 5)
Chrome.box(4, 12, 16, 6)
else
Chrome.box(0, 12, 20, 6)
end
if self.phase == "menu" then
self:printMessage()
local boxX = self.contest and CONTEST_MENU_BOX_X or MENU_BOX_X
@@ -3520,24 +3608,30 @@ function BattleState:drawPanel()
moves = (mon and mon.moves) or moves
end
local cursorRow = forgetting and self.forgetIndex or self.moveIndex
-- w2DMenuCursorInitX 5 with the names at hlcoord 6 (core.asm:5086-5107).
local cursorCol = moveMenu and 5 or 1
local nameCol = moveMenu and 6 or 2
for i, move in ipairs(moves) do
local ty = 13 + (i - 1)
-- Cursor in the box's own gutter, not clipped against the border.
if i == cursorRow then Chrome.cursor(1, ty) end
if i == cursorRow then Chrome.cursor(cursorCol, ty) end
-- The held slot's marker. `.battle_player_moves` writes '▷' into the
-- row wSwappingMove names (engine/battle/core.asm:5157-5165) so a move
-- picked up for a swap is visible while the cursor moves off it. It
-- sits a column right of the cursor gutter, where the cart puts it
-- (hlcoord 5, 13 against the cursor's own column), and only while the
-- move list itself is up -- the forget picker has no swapping.
if not forgetting and self.moveSwapIndex == i then
Chrome.print("\u{25B7}", 0, ty)
-- hlcoord 5, 13 is the cursor's own gutter, so PlaceMenuCursor covers
-- the marker on the cursor's row.
if not forgetting and self.moveSwapIndex == i and i ~= cursorRow then
Chrome.print("\u{25B7}", cursorCol, ty)
end
local def = self.game and self.game.data and self.game.data.moves
and self.game.data.moves[move.id]
Chrome.print((def and def.name) or move.id, 2, ty)
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0), 19, ty)
Chrome.print((def and def.name) or move.id, nameCol, ty)
if not moveMenu then
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
19, ty)
end
end
if moveMenu then self:drawMoveInfoBox(moves[cursorRow]) end
else
-- Battle messages wrap inside the box rather than running off the frame.
self:printMessage()
+6 -1
View File
@@ -337,10 +337,15 @@ function PackMenu:useSelected()
return
end
local world = self.world
local result = world and world.useFieldItem and world:useFieldItem(row.id)
local result, extra = nil, nil
if world and world.useFieldItem then result, extra = world:useFieldItem(row.id) end
if result then
if result == "nowhere" then
self.message = OAK_THIS_ISNT_THE_TIME
elseif result == "coin_case" then
-- _CoinCaseCountText (data/text/common_3.asm:336): "Coins:" then the
-- count, text_decimal 4 digits with PRINTNUM_LEFTALIGN_F so no padding.
self.message = { "Coins:", tostring(extra or 0) }
elseif result == "repel_used" then
-- ItemUsedText (data/text/common_3.asm): "<PLAYER> used the\n<ITEM>."
-- World already wrote the counter and took the item out of the bag, so
+14 -3
View File
@@ -150,7 +150,11 @@ function PokedexMenu.new(game, opts)
self.pokemon = opts.pokemon or data.pokemon
self.palettes = opts.palettes or data.gen2Palettes
self.onClose = opts.onClose
-- InitPokedex: wLastDexMode -> wCurDexMode (engine/pokedex/pokedex.asm:97).
self.modeIndex = 1
for i, name in ipairs(MODES) do
if self.save and name == self.save.lastDexMode then self.modeIndex = i end
end
self.index = 1
self.scroll = 0
self.view = "list" -- list | entry | area | option | search | results | unown
@@ -317,6 +321,13 @@ function PokedexMenu:cursorVisible()
return ((self.entryBlink or 0) % 32) < 20
end
-- Pokedex: wCurDexMode -> wLastDexMode on the way out
-- (engine/pokedex/pokedex.asm:60), which lives in the saved game data.
function PokedexMenu:close()
if self.save then self.save.lastDexMode = MODES[self.modeIndex] end
if self.onClose then self.onClose() end
end
function PokedexMenu:update(_dt)
self.entryBlink = (self.entryBlink or 0) + 1
local input = self.game and self.game.input
@@ -328,8 +339,8 @@ function PokedexMenu:update(_dt)
if input:wasPressed("a") or input:wasPressed("b") then
if self.page == 1 then
self.page = 2
elseif self.onClose then
self.onClose()
else
self:close()
end
end
return
@@ -367,7 +378,7 @@ function PokedexMenu:update(_dt)
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
self:close()
return
elseif input:wasPressed("select") then
-- Pokedex_UpdateMainScreen: SELECT opens the OPTION screen and START the
+3 -1
View File
@@ -2280,7 +2280,9 @@ function Pokegear:drawPanel()
self:drawClock()
end
-- Last: the arrow is an OBJ and composites over whatever the card drew.
self:drawModeArrow()
-- _FlyMap has no card strip and never animates it
-- (engine/pokegear/pokegear.asm:1999).
if not self.fly then self:drawModeArrow() end
G.setColor(1, 1, 1, 1)
end
+58 -8
View File
@@ -36,9 +36,8 @@
-- $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),
-- The extractor writes that sheet as menu_gfx.stats, which `pageTile` draws.
-- 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).
@@ -95,6 +94,14 @@ local TILE_BAR_CAP_RIGHT = 0x41
-- made of (StatsScreen_PlaceHorizontalDivider).
local TILE_HORIZONTAL_DIVIDER = 0x62
-- gfx/stats/pages.pal, the three palettes _CGB_StatsScreenHPPals copies to
-- wBGPals1 slots 3-5 (engine/gfx/cgb_layouts.asm:199-212)
local PAGE_PALETTES = {
{ { 255, 255, 255 }, { 255, 156, 255 }, { 255, 123, 255 }, { 0, 0, 0 } },
{ { 255, 255, 255 }, { 173, 255, 115 }, { 140, 255, 0 }, { 0, 0, 0 } },
{ { 255, 255, 255 }, { 140, 255, 255 }, { 140, 255, 255 }, { 0, 0, 0 } },
}
-- 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.
@@ -821,12 +828,37 @@ 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.
-- menu_gfx.stats, the 17 tiles LoadStatsScreenPageTilesGFX lands at vTiles2
-- tile $31 (engine/gfx/load_font.asm:90-95)
function SummaryMenu:statsTiles()
if self.statsSheet ~= nil then return self.statsSheet or nil end
local gfx = (self.menuGfx or {}).stats
local image = gfx and self:picImage(gfx.sheet)
if not image then
self.statsSheet = false
return nil
end
local w, h = image:getDimensions()
local quads = {}
for index = 0, (gfx.tiles or 17) - 1 do
quads[(gfx.firstTile or 0x31) + index] =
love.graphics.newQuad(index * 8, 0, 8, 8, w, h)
end
self.statsSheet = { image = image, quads = quads }
return self.statsSheet
end
-- A tile out of StatsScreenPageTilesGFX. The fallback arm draws each of the
-- seven shapes by hand for a cache built before menu_gfx.stats existed.
function SummaryMenu:pageTile(id, tx, ty)
local G = love.graphics
local px, py = tx * 8, ty * 8
local sheet = self:statsTiles()
if sheet and sheet.quads[id] then
G.setColor(1, 1, 1, 1)
G.draw(sheet.image, sheet.quads[id], px, py)
return
end
G.setColor(0, 0, 0, 1)
if id == TILE_VERTICAL_DIVIDER then
G.rectangle("fill", px + 3, py, 2, 8)
@@ -847,11 +879,29 @@ end
-- (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)
function SummaryMenu:drawPageSquare(tx, ty, large, colors)
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 sheet = self:statsTiles()
if sheet and sheet.quads[first] then
-- [hli] / [hld], a row down, [hli] / [hl]: the four tiles in that order.
local function body()
G.setColor(1, 1, 1, 1)
G.draw(sheet.image, sheet.quads[first], px, py)
G.draw(sheet.image, sheet.quads[first + 1], px + 8, py)
G.draw(sheet.image, sheet.quads[first + 2], px, py + 8)
G.draw(sheet.image, sheet.quads[first + 3], px + 8, py + 8)
end
if colors and GbcPalette.available() then
GbcPalette.with(colors, body)
else
body()
end
G.setColor(1, 1, 1, 1)
return
end
local inset = first == TILE_SQUARE_LARGE and 2 or 5
local size = 16 - inset * 2
G.setColor(0, 0, 0, 1)
@@ -862,7 +912,7 @@ end
function SummaryMenu:drawPageIndicators()
local columns = { 13, 15, 17 }
for i, tx in ipairs(columns) do
self:drawPageSquare(tx, 5, i == self.page)
self:drawPageSquare(tx, 5, i == self.page, PAGE_PALETTES[i])
end
end