mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
CLOSES #1396, CLOSES #1398, CLOSES #1400, CLOSES #1401, CLOSES #1406, CLOSES #1407, CLOSES #1411, CLOSES #1413, CLOSES #1415, CLOSES #1416, CLOSES #1417, CLOSES #1419, CLOSES #1421, CLOSES #1422, CLOSES #1423, CLOSES #1424, CLOSES #1425, CLOSES #1427, CLOSES #1428, CLOSES #1429, CLOSES #1431, CLOSES #1432, CLOSES #1433, CLOSES #1435, CLOSES #1437, CLOSES #1440, CLOSES #1441, CLOSES #1442, CLOSES #1443, CLOSES #1447, CLOSES #1449, CLOSES #1456, CLOSES #1464, CLOSES #1465, CLOSES #1468, CLOSES #1469, CLOSES #1470
This commit is contained in:
+74
-18
@@ -48,6 +48,18 @@ local TILE_PLAYER_BOTTOM_LEFT = 0x6f
|
||||
-- (engine/battle/trainer_huds.asm:143-152).
|
||||
local TILE_CAUGHT = 0x5d
|
||||
|
||||
-- The ball icons StageBallTilesData stages, one per party slot
|
||||
-- (engine/battle/trainer_huds.asm:47-99).
|
||||
local TILE_BALL_NORMAL = 0x31
|
||||
local TILE_BALL_STATUSED = 0x32
|
||||
local TILE_BALL_FAINTED = 0x33
|
||||
local TILE_BALL_EMPTY = 0x34
|
||||
-- DrawPlayerPartyIconHUDBorder's corner (trainer_huds.asm:118-132), which is
|
||||
-- DrawPlayerHUDBorder's with $77 swapped for $5c.
|
||||
local TILE_PARTY_ICON_BOTTOM_RIGHT = 0x5c
|
||||
|
||||
BattleHud.PARTY_LENGTH = 6
|
||||
|
||||
function BattleHud.new(menuGfx, palettes)
|
||||
local self = setmetatable({}, BattleHud)
|
||||
self.gfx = menuGfx and menuGfx.battleHud or nil
|
||||
@@ -184,18 +196,25 @@ local TILE_EXP_FULL = 0x6a -- FontBattleExtra
|
||||
local TILE_EXP_EMPTY = 0x62 -- FontBattleExtra
|
||||
local EXP_PARTIAL_BASE = 0x54 -- $54 + remainder lands in ExpBarGFX
|
||||
|
||||
-- PAL_BATTLE_BG_EXP, which the attrmap lays over (10,11)..(18,11)
|
||||
-- (engine/gfx/cgb_layouts.asm:142-145).
|
||||
function BattleHud:expColors()
|
||||
local pal = self.palettes and self.palettes.expBar
|
||||
if not pal then return nil end
|
||||
return {
|
||||
{ 255, 255, 255 },
|
||||
{ pal[1][1], pal[1][2], pal[1][3] },
|
||||
{ pal[2][1], pal[2][2], pal[2][3] },
|
||||
{ 0, 0, 0 },
|
||||
}
|
||||
end
|
||||
|
||||
function BattleHud:drawExpBar(fraction, tx, ty)
|
||||
if not self:image("hpBar") then return false end
|
||||
fraction = math.max(0, math.min(1, fraction or 0))
|
||||
local pixels = math.floor(fraction * BattleHud.EXP_LENGTH_PX)
|
||||
-- The whole row wears the exp bar's palette, full and empty cells included.
|
||||
local pal = self.palettes and self.palettes.expBar
|
||||
local colors = pal and {
|
||||
{ 255, 255, 255 },
|
||||
{ pal[1][1], pal[1][2], pal[1][3] },
|
||||
{ pal[2][1], pal[2][2], pal[2][3] },
|
||||
{ 0, 0, 0 },
|
||||
} or nil
|
||||
local colors = self:expColors()
|
||||
|
||||
local remaining = pixels
|
||||
for cell = BattleHud.EXP_CELLS - 1, 0, -1 do
|
||||
@@ -269,19 +288,56 @@ end
|
||||
-- DrawPlayerHUDBorder: hlcoord 18, 10 stepping LEFT, tiles $73 / $77 / $6f /
|
||||
-- $76, plus the extra vertical bar DrawPlayerHUD writes at (18,9) so the stub
|
||||
-- is two rows tall.
|
||||
local PLAYER_FRAME_TILES = {
|
||||
sideSheet = "playerBorder", sideFirst = PLAYER_BORDER_FIRST,
|
||||
cornerSheet = "playerBorder", cornerFirst = PLAYER_BORDER_FIRST,
|
||||
-- $6f is the LAST tile of EnemyHPBarBorderGFX, not the player sheet
|
||||
-- (engine/gfx/load_font.asm:57-65).
|
||||
farSheet = "enemyBorder", farFirst = ENEMY_BORDER_FIRST,
|
||||
side = TILE_PLAYER_RIGHT,
|
||||
nearCorner = TILE_PLAYER_BOTTOM_RIGHT,
|
||||
farCorner = TILE_PLAYER_BOTTOM_LEFT,
|
||||
bottom = TILE_BOTTOM_SIDE,
|
||||
}
|
||||
|
||||
function BattleHud:drawPlayerFrame()
|
||||
self:drawTile("playerBorder", PLAYER_BORDER_FIRST, TILE_PLAYER_RIGHT, 18, 9)
|
||||
self:placeBorder({
|
||||
sideSheet = "playerBorder", sideFirst = PLAYER_BORDER_FIRST,
|
||||
cornerSheet = "playerBorder", cornerFirst = PLAYER_BORDER_FIRST,
|
||||
-- $6f is the LAST tile of EnemyHPBarBorderGFX, not the player sheet
|
||||
-- (engine/gfx/load_font.asm:57-65).
|
||||
farSheet = "enemyBorder", farFirst = ENEMY_BORDER_FIRST,
|
||||
side = TILE_PLAYER_RIGHT,
|
||||
nearCorner = TILE_PLAYER_BOTTOM_RIGHT,
|
||||
farCorner = TILE_PLAYER_BOTTOM_LEFT,
|
||||
bottom = TILE_BOTTOM_SIDE,
|
||||
}, 18, 10, -1)
|
||||
self:placeBorder(PLAYER_FRAME_TILES, 18, 10, -1)
|
||||
end
|
||||
|
||||
-- DrawPlayerPartyIconHUDBorder (engine/battle/trainer_huds.asm:118-132): the
|
||||
-- player border with $5c for the bottom right, and no bar at (18,9).
|
||||
function BattleHud:drawPartyIconFrame()
|
||||
self:placeBorder(PLAYER_FRAME_TILES, 18, 10, -1)
|
||||
return self:drawTile("expBar", self.gfx and self.gfx.expBarFirstTile,
|
||||
TILE_PARTY_ICON_BOTTOM_RIGHT, 18, 11, self:expColors())
|
||||
end
|
||||
|
||||
-- StageBallTilesData's .GetHUDTile, and the $34 it stages past the party
|
||||
-- count (engine/battle/trainer_huds.asm:47-100).
|
||||
local function ballTile(mon)
|
||||
if not mon then return TILE_BALL_EMPTY end
|
||||
if (mon.hp or 0) <= 0 then return TILE_BALL_FAINTED end
|
||||
return mon.status and TILE_BALL_STATUSED or TILE_BALL_NORMAL
|
||||
end
|
||||
|
||||
-- PAL_BATTLE_OB_YELLOW (engine/battle/trainer_huds.asm:213-214).
|
||||
function BattleHud:ballColors()
|
||||
local pals = self.palettes and self.palettes.battleObjects
|
||||
return pals and pals.PAL_BATTLE_OB_YELLOW or nil
|
||||
end
|
||||
|
||||
-- LoadTrainerHudOAM (engine/battle/trainer_huds.asm:203-223): six sprites
|
||||
-- from (tx, ty), each one tile further along `step`.
|
||||
function BattleHud:drawBallRow(party, tx, ty, step)
|
||||
if not self:image("balls") then return false end
|
||||
local first = self.gfx.ballsFirstTile or TILE_BALL_NORMAL
|
||||
local colors = self:ballColors()
|
||||
for slot = 1, BattleHud.PARTY_LENGTH do
|
||||
self:drawTile("balls", first, ballTile(party and party[slot]),
|
||||
tx + (slot - 1) * step, ty, colors)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
BattleHud.TILE_HP_LABEL = TILE_HP_LABEL
|
||||
|
||||
+106
-10
@@ -314,6 +314,13 @@ function BattleState.new(game, opts)
|
||||
self.showEnemyHud = false
|
||||
self.showPlayerHud = false
|
||||
|
||||
-- BattleStart_TrainerHuds, farcalled from BattleStartMessage before the
|
||||
-- opening line (engine/battle/trainer_huds.asm:1-9, core.asm:8733).
|
||||
self.ballRows = {
|
||||
player = true,
|
||||
enemy = not (self.battle and self.battle.wild),
|
||||
}
|
||||
|
||||
-- InitEnemyTrainer (engine/battle/core.asm:7848) puts the CLASS's 7x7
|
||||
-- frontpic in the enemy pic box BEFORE the intro slide, and it stays there
|
||||
-- until ResetEnemyBattleVars slides it off; only then is the mon drawn. The
|
||||
@@ -349,6 +356,7 @@ function BattleState.new(game, opts)
|
||||
|
||||
local enemy = self.battle and self.battle.enemy
|
||||
self:noteFirstUnown(enemy)
|
||||
self:markSeen(enemy)
|
||||
if enemy then
|
||||
if self.battle.wild then
|
||||
-- BattleCheckEnemyShininess: a shiny wild mon gets ANIM_SEND_OUT_MON's
|
||||
@@ -399,6 +407,13 @@ function BattleState.new(game, opts)
|
||||
-- "X fainted!" is even displayed. The replacement arrives with its own
|
||||
-- `send` event, which is where the cart's send-out animation sits.
|
||||
self.shownMon = { player = player, enemy = enemy }
|
||||
-- home/battle.asm:150 UpdateBattleHuds
|
||||
self.shownStatus = {
|
||||
player = (player and player.status) or false,
|
||||
enemy = (enemy and enemy.status) or false,
|
||||
}
|
||||
-- engine/battle/trainer_huds.asm:142-151
|
||||
self.caughtMark = self:dexCaught(enemy)
|
||||
-- And the same for the two numbers AnimateExpBar walks: wBattleMonLevel is
|
||||
-- only advanced inside its level loop, right after that level's bar has
|
||||
-- crawled full (engine/battle/core.asm:7267-7274), so neither the level nor
|
||||
@@ -443,6 +458,16 @@ function BattleState:noteFirstUnown(mon)
|
||||
save.firstUnownSeen = Unown.monLetter(mon)
|
||||
end
|
||||
|
||||
-- LoadEnemyMon's "Saw this mon" (engine/battle/core.asm:6203-6209): the seen
|
||||
-- flag is stamped for every battle mode, so a trainer's mon counts too.
|
||||
function BattleState:markSeen(mon)
|
||||
local save = self.save
|
||||
if not (save and mon and mon.species) then return end
|
||||
save.pokedex = save.pokedex or { seen = {}, caught = {} }
|
||||
save.pokedex.seen = save.pokedex.seen or {}
|
||||
save.pokedex.seen[mon.species] = true
|
||||
end
|
||||
|
||||
-- The DUDE answering a prompt. Every re-arm in the ASM sits at the moment the
|
||||
-- cart starts WAITING for a button (`.wait_input` in home/joypad.asm, BattleMenu
|
||||
-- before LoadBattleMenu, TutorialPack before its own loop), so each one goes
|
||||
@@ -936,6 +961,25 @@ function BattleState:dexCaught(mon)
|
||||
return (mon and caught and caught[mon.species]) and true or false
|
||||
end
|
||||
|
||||
-- The status the HUD prints, one drain behind the engine the way shownHp is:
|
||||
-- UpdateBattleHuds runs after the animation and its line (home/battle.asm:150).
|
||||
function BattleState:hudStatus(mon, side)
|
||||
local shown = side and self.shownStatus and self.shownStatus[side]
|
||||
if shown == nil then return mon and mon.status or nil end
|
||||
return shown or nil
|
||||
end
|
||||
|
||||
-- home/battle.asm:150, for the clears no queued event carries (a mon waking,
|
||||
-- a thaw, a bag cure): the tags catch up once the queue is idle.
|
||||
function BattleState:syncShownStatus()
|
||||
local shown, battle = self.shownStatus, self.battle
|
||||
if not (shown and battle) then return end
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local mon = battle[side]
|
||||
shown[side] = (mon and mon.status) or false
|
||||
end
|
||||
end
|
||||
|
||||
-- The low-HP alarm is not an SFX id at all. PlayDanger (audio/engine.asm:531)
|
||||
-- runs every frame while DANGER_ON_F is set in wLowHealthAlarm and writes a
|
||||
-- two-tone square straight to channel 1 -- DangerSoundHigh ($750) at counter 0,
|
||||
@@ -1153,7 +1197,7 @@ function BattleState:stepAnim(input)
|
||||
-- Cart still reaches the after-anim arm after a move script ends; a skip
|
||||
-- of the move should not drop the hit shake that follows it.
|
||||
if self:startPendingAfterAnim() then return end
|
||||
return self:endSendOutAnim()
|
||||
return self:endSendOutAnim(true)
|
||||
end
|
||||
if not self.anim:step() then
|
||||
self:latchCaughtPic()
|
||||
@@ -1170,10 +1214,17 @@ end
|
||||
|
||||
-- Whatever Call_PlayBattleAnim was standing in front of: a send-out's cry and
|
||||
-- HUD update run the moment its animation is done, cut short or not.
|
||||
function BattleState:endSendOutAnim()
|
||||
function BattleState:endSendOutAnim(skipped)
|
||||
local after = self.afterSendOut
|
||||
if not after then return end
|
||||
self.afterSendOut = nil
|
||||
if after.shiny and not skipped then
|
||||
after.shiny = nil
|
||||
if self:animForId("ANIM_SEND_OUT_MON", after.side, 1) then
|
||||
self.afterSendOut = after
|
||||
return
|
||||
end
|
||||
end
|
||||
self:finishSendOut(after)
|
||||
end
|
||||
|
||||
@@ -1202,6 +1253,7 @@ function BattleState:advanceQueue()
|
||||
self.introTextShown = nil
|
||||
end
|
||||
if not event then
|
||||
self:syncShownStatus()
|
||||
-- `jp PlayerSwitch`, which follows the enemy's own send-out and spends no
|
||||
-- turn (engine/battle/core.asm:2955-2963).
|
||||
if self.shiftSwitchIndex then
|
||||
@@ -1315,6 +1367,14 @@ function BattleState:advanceQueue()
|
||||
self.shownHp[event.side] = 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
|
||||
-- to the incoming mon.
|
||||
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
|
||||
end
|
||||
-- AnimateExpBar (engine/battle/core.asm:7191) is called from INSIDE
|
||||
-- GiveExperiencePoints before the exp is committed (the call at :6888 sits
|
||||
-- ahead of the commit at :6889-6901), so the bar crawls from the figures
|
||||
@@ -1339,7 +1399,12 @@ function BattleState:advanceQueue()
|
||||
-- is still on screen for its own line and the replacement arrives here.
|
||||
if self.shownMon then self.shownMon[event.side] = event.mon end
|
||||
-- The second of the cart's two wFirstUnownSeen writes (core.asm:3251).
|
||||
if event.side == "enemy" then self:noteFirstUnown(event.mon) end
|
||||
if event.side == "enemy" then
|
||||
self:noteFirstUnown(event.mon)
|
||||
self:markSeen(event.mon)
|
||||
-- engine/battle/trainer_huds.asm:142-151
|
||||
self.caughtMark = self:dexCaught(event.mon)
|
||||
end
|
||||
if event.side == "player" then
|
||||
-- SendOutPlayerMon zeroes wBattleMenuCursorPosition and wCurMoveNum back
|
||||
-- to back (engine/battle/core.asm:3809), so a switched-in mon opens on
|
||||
@@ -1535,12 +1600,18 @@ end
|
||||
-- SetEnemyTurn / SetPlayerTurn, then ANIM_SEND_OUT_MON. The cry and the HUD
|
||||
-- come after the animation, not with it.
|
||||
function BattleState:startSendOut(side, mon)
|
||||
local after = { side = side, mon = mon }
|
||||
-- BattleCheckPlayerShininess / BattleCheckEnemyShininess replay the anim's
|
||||
-- `.Shiny` arm before the cry (core.asm:3826-3831, :3371-3377).
|
||||
local after = { side = side, mon = mon, shiny = mon and mon.shiny and true }
|
||||
-- ShowSetEnemyMonAndSendOutAnimation and SendOutPlayerMon both draw the pic
|
||||
-- into the box before they play the animation, which is the one thing that
|
||||
-- undoes a cleared box.
|
||||
self.picHidden[side] = false
|
||||
self.faintSlide = nil
|
||||
-- The rows are shadow OAM (engine/battle/trainer_huds.asm:203-223), which
|
||||
-- this animation's own sprites overwrite.
|
||||
self.ballRows.player = false
|
||||
self.ballRows.enemy = false
|
||||
if self:animForId("ANIM_SEND_OUT_MON", side) then
|
||||
self.afterSendOut = after
|
||||
return true
|
||||
@@ -2569,6 +2640,9 @@ function BattleState:pushCaught(enemy, itemId)
|
||||
-- in the box", item_effects.asm:624). Boxes.deposit stays an append: the
|
||||
-- PC's own move is InsertPokemonIntoBox, which inserts at the cursor.
|
||||
table.insert(box, 1, enemy)
|
||||
-- SendMonIntoBox refills the boxed slot's PP before it closes SRAM
|
||||
-- (move_mon.asm:1062-1063).
|
||||
Boxes.restorePP(enemy)
|
||||
-- `.SendToPC` re-reads sBoxCount AFTER the insert and sets
|
||||
-- BATTLERESULT_BOX_FULL when the box has just filled
|
||||
-- (item_effects.asm:612-619); Script_reloadmapafterbattle tests that bit
|
||||
@@ -2638,6 +2712,9 @@ end
|
||||
-- (engine/battle/core.asm:3298-3304, data/text/battle.asm:222-231).
|
||||
function BattleState:offerShiftSwitch(mon)
|
||||
self.shiftIndex = 1
|
||||
-- HandleEnemySwitch farcalls EnemySwitch_TrainerHud before the prompt
|
||||
-- (engine/battle/core.asm:2246, engine/battle/trainer_huds.asm:11-15).
|
||||
self.ballRows.enemy = true
|
||||
local trainer = (self.battle.trainer and self.battle.trainer.name) or "Foe"
|
||||
local player = (self.save and self.save.player and self.save.player.name)
|
||||
or "GOLD"
|
||||
@@ -2921,6 +2998,9 @@ function BattleState:useItem(itemId)
|
||||
end
|
||||
local player = self.battle.player
|
||||
caught, rate = Catching.attempt({
|
||||
battle = self.battle,
|
||||
mon = enemy,
|
||||
def = enemyDef,
|
||||
maxHp = enemy.maxHp or (enemy.stats and enemy.stats.hp),
|
||||
hp = enemy.hp,
|
||||
catchRate = enemyDef and enemyDef.catchRate or 45,
|
||||
@@ -3231,13 +3311,13 @@ function BattleState:drawHud()
|
||||
-- PlaceNonFaintStatus (engine/pokemon/mon_stats.asm): a statused mon's tag
|
||||
-- prints where the level goes, and DrawEnemyHUD's `.skip_level` arm drops
|
||||
-- the level entirely while one is up.
|
||||
Chrome.print(self:statusTag(enemy) or ("<LV>" .. tostring(enemy.level or 1)),
|
||||
6, 1)
|
||||
Chrome.print(self:statusTag(enemy, "enemy")
|
||||
or ("<LV>" .. tostring(enemy.level or 1)), 6, 1)
|
||||
local enemyGender = self:genderSymbol(enemy)
|
||||
if enemyGender then Chrome.print(enemyGender, 9, 1) end
|
||||
-- `ld a, [wBattleMode] / dec a / ret nz`, then CheckCaughtMon puts $5d at
|
||||
-- (1,1) (engine/battle/trainer_huds.asm:140-152).
|
||||
if self.battle and self.battle.wild and self:dexCaught(enemy) then
|
||||
if self.battle and self.battle.wild and self.caughtMark then
|
||||
self.hud:drawCaughtIcon(1, 1, self:hudHp(enemy, "enemy"),
|
||||
enemy.maxHp or (enemy.stats and enemy.stats.hp))
|
||||
end
|
||||
@@ -3249,6 +3329,14 @@ function BattleState:drawHud()
|
||||
self:drawFrame(1, 3, 10, false)
|
||||
end
|
||||
end
|
||||
-- ShowOTTrainerMonsRemaining (engine/battle/trainer_huds.asm:32-45): balls
|
||||
-- walking LEFT from OAM (72, 32), which the -8/-16 offset puts at (8, 2).
|
||||
if showStatus and self.ballRows.enemy then
|
||||
if not self.showEnemyHud and self.hud:available() then
|
||||
self.hud:drawEnemyFrame()
|
||||
end
|
||||
self.hud:drawBallRow(self.battle and self.battle.enemyParty, 8, 2, -1)
|
||||
end
|
||||
|
||||
self:drawPic(enemy, false)
|
||||
|
||||
@@ -3257,6 +3345,14 @@ function BattleState:drawHud()
|
||||
-- the HP bar at (10,9), its numbers below; the vertical bar at (18,9), the
|
||||
-- border from (18,10) going left, and the exp bar at (10,11).
|
||||
self:drawPic(player, true)
|
||||
-- ShowPlayerMonsRemaining (engine/battle/trainer_huds.asm:17-30): balls
|
||||
-- walking RIGHT from OAM (96, 96), i.e. tile (11, 10).
|
||||
if showStatus and self.ballRows.player then
|
||||
if not self.showPlayerHud and self.hud:available() then
|
||||
self.hud:drawPartyIconFrame()
|
||||
end
|
||||
self.hud:drawBallRow(self.battle and self.battle.party, 11, 10, 1)
|
||||
end
|
||||
-- No player HUD in the catching tutorial: DrawPlayerHUD lives in
|
||||
-- SendOutPlayerMon, which BATTLETYPE_TUTORIAL jumps straight over, and
|
||||
-- BattleMenu's own tutorial arm skips UpdateBattleHuds as well. The DUDE's
|
||||
@@ -3270,7 +3366,7 @@ function BattleState:drawHud()
|
||||
Chrome.print(self:name(player), 10, 7)
|
||||
-- PrintPlayerHUD places the same status tag at (14,8) and skips the level
|
||||
-- while it is up.
|
||||
Chrome.print(self:statusTag(player)
|
||||
Chrome.print(self:statusTag(player, "player")
|
||||
or ("<LV>" .. tostring(self.shownLevel or player.level or 1)), 14, 8)
|
||||
local playerGender = self:genderSymbol(player)
|
||||
if playerGender then Chrome.print(playerGender, 17, 8) end
|
||||
@@ -3303,8 +3399,8 @@ local STATUS_TAGS = {
|
||||
paralyze = "PAR", sleep = "SLP",
|
||||
}
|
||||
|
||||
function BattleState:statusTag(mon)
|
||||
return mon and STATUS_TAGS[mon.status] or nil
|
||||
function BattleState:statusTag(mon, side)
|
||||
return mon and STATUS_TAGS[self:hudStatus(mon, side)] or nil
|
||||
end
|
||||
|
||||
-- ♂ / ♀ after the level, or nil for a genderless species (PrintPlayerHUD
|
||||
|
||||
+47
-18
@@ -78,8 +78,12 @@ local MOVE_SUBMENU = { "MOVE", "STATS", "CANCEL" }
|
||||
-- engine/pokemon/bills_pc.asm:472-478: BillsPC_Withdraw's menu rows.
|
||||
local WITHDRAW_SUBMENU = { "WITHDRAW", "STATS", "RELEASE", "CANCEL" }
|
||||
|
||||
-- BillsPCDepositMenuHeader's .MenuData (engine/pokemon/bills_pc.asm:234-240).
|
||||
local DEPOSIT_SUBMENU = { "DEPOSIT", "STATS", "RELEASE", "CANCEL" }
|
||||
|
||||
function BoxMenu:submenuRows()
|
||||
if self.mode == "move" then return MOVE_SUBMENU end
|
||||
if self.mode == "deposit" then return DEPOSIT_SUBMENU end
|
||||
return WITHDRAW_SUBMENU
|
||||
end
|
||||
|
||||
@@ -219,21 +223,12 @@ function BoxMenu:act()
|
||||
if self.onClose then self.onClose() end
|
||||
return
|
||||
end
|
||||
-- engine/pokemon/bills_pc.asm:336-344: withdraw and move both PrepSubmenu.
|
||||
if self.mode == "move" or self.mode == "withdraw" then
|
||||
if not self:selected() then return end
|
||||
self.phase = "submenu"
|
||||
-- `ld a, $1 / ld [wMenuCursorY], a`: the submenu always opens on MOVE.
|
||||
self.submenuIndex = 1
|
||||
return
|
||||
end
|
||||
local ok, result = Boxes.deposit(self.save, self.index, self.boxIndex)
|
||||
if not ok then
|
||||
self.message = result
|
||||
return
|
||||
end
|
||||
self.message = nil
|
||||
self:clampIndex()
|
||||
-- engine/pokemon/bills_pc.asm:336-344: withdraw and move both PrepSubmenu,
|
||||
-- and _DepositPKMN's .a_button steps to .WhatsUp the same way (:94-102).
|
||||
if not self:selected() then return end
|
||||
self.phase = "submenu"
|
||||
-- `ld a, $1 / ld [wMenuCursorY], a`: the submenu always opens on its top row.
|
||||
self.submenuIndex = 1
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------ MOVE, step 2
|
||||
@@ -243,8 +238,9 @@ end
|
||||
-- the PARTY, so a boxed mon walks straight past. Returns true, or false and
|
||||
-- the string the cart places.
|
||||
function BoxMenu:checkMailPreventBlackout()
|
||||
-- `ld a, [wBillsPC_LoadedBox] / and a / jr nz, .Okay`.
|
||||
if not self:isParty() then return true end
|
||||
-- `ld a, [wBillsPC_LoadedBox] / and a / jr nz, .Okay`; _DepositPKMN zeroes
|
||||
-- that byte too, so its list is the party (bills_pc.asm:17-18).
|
||||
if not (self:isParty() or self.mode == "deposit") then return true end
|
||||
local party = self.save.party or {}
|
||||
-- `cp $3 / jr c, .ItsYourLastPokemon`: a party of one or two may not send
|
||||
-- one away at all, however healthy the rest of it is.
|
||||
@@ -311,10 +307,25 @@ function BoxMenu:doWithdraw()
|
||||
self:clampIndex()
|
||||
end
|
||||
|
||||
-- engine/pokemon/bills_pc.asm:155 BillsPCDepositFuncDeposit
|
||||
function BoxMenu:doDeposit()
|
||||
local ok, result = Boxes.deposit(self.save, self.index, self.boxIndex)
|
||||
if not ok then
|
||||
self.message = result
|
||||
return
|
||||
end
|
||||
self.message = nil
|
||||
self.phase = nil
|
||||
self.index, self.scroll = 1, 0
|
||||
self:clampIndex()
|
||||
end
|
||||
|
||||
function BoxMenu:chooseSubmenu()
|
||||
local row = self:submenuRows()[self.submenuIndex]
|
||||
if row == "MOVE" then
|
||||
self:beginMove()
|
||||
elseif row == "DEPOSIT" then
|
||||
self:doDeposit()
|
||||
elseif row == "WITHDRAW" then
|
||||
self:doWithdraw()
|
||||
elseif row == "STATS" then
|
||||
@@ -393,6 +404,9 @@ function BoxMenu:insertMon()
|
||||
target = target - 1
|
||||
end
|
||||
table.insert(dest, math.max(1, math.min(target, #dest + 1)), mon)
|
||||
-- .CopyToBox is InsertPokemonIntoBox, which tails into
|
||||
-- RestorePPOfDepositedPokemon (engine/pokemon/move_mon_wo_mail.asm:35-37).
|
||||
if not self:isParty(destIndex) then Boxes.restorePP(mon) end
|
||||
self.phase = nil
|
||||
self.moveFrom, self.backup = nil, nil
|
||||
self.index, self.scroll = 1, 0
|
||||
@@ -521,6 +535,16 @@ function BoxMenu:askRelease()
|
||||
if self:isCancel() then return end
|
||||
local mon = self:selected()
|
||||
if not mon then return end
|
||||
-- BillsPCDepositFuncRelease runs the mail/blackout net BEFORE the egg check
|
||||
-- (engine/pokemon/bills_pc.asm:183-187).
|
||||
if self.mode == "deposit" then
|
||||
local allowed, refusal = self:checkMailPreventBlackout()
|
||||
if not allowed then
|
||||
self.phase = nil
|
||||
self.message = refusal
|
||||
return
|
||||
end
|
||||
end
|
||||
-- Both release paths run BillsPC_IsMonAnEgg first, so the question is never
|
||||
-- even asked over an egg (engine/pokemon/bills_pc.asm:186-187 and :427-428).
|
||||
if mon.isEgg then
|
||||
@@ -534,7 +558,12 @@ function BoxMenu:askRelease()
|
||||
local name = mon.nickname or mon.name or mon.species or "?"
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then return end
|
||||
local ok, err = Boxes.release(self.save, self.boxIndex, self.index)
|
||||
local ok, err
|
||||
if self.mode == "deposit" then
|
||||
ok, err = Boxes.releaseFromParty(self.save, self.index)
|
||||
else
|
||||
ok, err = Boxes.release(self.save, self.boxIndex, self.index)
|
||||
end
|
||||
if not ok then
|
||||
self.message = err
|
||||
return
|
||||
|
||||
@@ -518,8 +518,9 @@ function ItemPcMenu:drawList()
|
||||
local entry = self.rows[i]
|
||||
if i == self.listIndex then Chrome.cursor(5, ty) end
|
||||
Chrome.print(entry.name, 6, ty)
|
||||
-- PlaceMenuItemQuantity: the xNN is the entry's second line.
|
||||
Chrome.print(TIMES .. tostring(entry.count), 7, ty + 1)
|
||||
-- PlaceMenuItemQuantity (engine/menus/menu_2.asm:24): the xNN is the
|
||||
-- entry's second line, right-aligned in a blank-padded 2-digit field.
|
||||
Chrome.print(TIMES .. Chrome.number(entry.count, 2), 7, ty + 1)
|
||||
elseif i == self:listTotal() then
|
||||
if i == self.listIndex then Chrome.cursor(5, ty) end
|
||||
Chrome.print("CANCEL", 6, ty)
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
-- * The two tilemaps behind DrawMagnetTrain (MagnetTrainBGTiles, a 2x18
|
||||
-- vertical strip repeated across all 32 columns, and MagnetTrainTilemap,
|
||||
-- the 20x4 train laid over rows 6-9) come from the extracted cache at
|
||||
-- data.field.magnetTrain. A cache built before the extractor learned to
|
||||
-- follow them has neither, and then the ride runs with a blank screen
|
||||
-- rather than with invented art.
|
||||
-- data.gen2Field.magnetTrain. A cache built before the extractor
|
||||
-- learned to follow them has neither, and then the ride runs with a
|
||||
-- blank screen rather than with invented art.
|
||||
-- * The background is baked into a 256x144 canvas once, because the only
|
||||
-- thing that changes per frame is the per-band SCX.
|
||||
-- * SetMagnetTrainPals gives the four bush rows and the four bottom rows
|
||||
@@ -57,7 +57,7 @@ function MagnetTrainRide.new(game, opts)
|
||||
self.onDone = opts.onDone
|
||||
self.finished = false
|
||||
|
||||
local field = self.data and self.data.field
|
||||
local field = self.data and self.data.gen2Field
|
||||
local gfx = field and field.magnetTrain
|
||||
self.ride = MagnetTrain.new({
|
||||
toGoldenrod = opts.toGoldenrod,
|
||||
@@ -65,8 +65,8 @@ function MagnetTrainRide.new(game, opts)
|
||||
fgTilemap = gfx and gfx.tilemap,
|
||||
})
|
||||
|
||||
self.tileset = self.data and self.data.tilesets
|
||||
and self.data.tilesets.TILESET_TRAIN_STATION
|
||||
self.tileset = self.data and self.data.gen2Tilesets
|
||||
and self.data.gen2Tilesets.TILESET_TRAIN_STATION
|
||||
self.palettes = self:bgPalettes()
|
||||
self.spriteSheet = self:playerSheet()
|
||||
|
||||
@@ -84,7 +84,7 @@ end
|
||||
-- even though both stations are INDOOR maps.
|
||||
function MagnetTrainRide:bgPalettes()
|
||||
local data = self.data
|
||||
local palettes = data and data.palettes
|
||||
local palettes = data and data.gen2Palettes
|
||||
if not palettes then return nil end
|
||||
return Palettes.bgSet(palettes, { environment = "TOWN" },
|
||||
Palettes.clockDaytime())
|
||||
@@ -129,7 +129,7 @@ end
|
||||
-- are cut per 8x8 sub-tile rather than by the sheet's 16-pixel width, because
|
||||
-- the OAM data addresses the four tiles of a frame individually.
|
||||
function MagnetTrainRide:playerSheet()
|
||||
local sprites = self.data and self.data.sprites
|
||||
local sprites = self.data and self.data.gen2Sprites
|
||||
local def = sprites and (sprites.SPRITE_CHRIS or sprites.SPRITE_KRIS)
|
||||
local path = def and def.image
|
||||
if not path then return nil end
|
||||
@@ -259,8 +259,8 @@ end
|
||||
-- MapObjectPals' PAL_OW_RED, the palette every .OAMData_MagnetTrainRed entry
|
||||
-- names.
|
||||
function MagnetTrainRide:playerPalette()
|
||||
local palettes = self.data and self.data.palettes
|
||||
local sprites = self.data and self.data.sprites
|
||||
local palettes = self.data and self.data.gen2Palettes
|
||||
local sprites = self.data and self.data.gen2Sprites
|
||||
if not palettes then return nil end
|
||||
return Palettes.spritePalette(palettes, Palettes.clockDaytime(),
|
||||
sprites and sprites.SPRITE_CHRIS)
|
||||
|
||||
@@ -211,6 +211,15 @@ local ROWS = {
|
||||
TC.buzz(options.haptics)
|
||||
if game and game.persistOptions then game:persistOptions() end
|
||||
end },
|
||||
{ label = "MAX FPS", key = "fpsCap", port = true,
|
||||
cycle = function(options, delta)
|
||||
local FrameCap = require("src.core.FrameCap")
|
||||
options.fpsCap = FrameCap.cycle(options.fpsCap, delta)
|
||||
FrameCap.apply(options.fpsCap)
|
||||
end,
|
||||
text = function(options)
|
||||
return require("src.core.FrameCap").label(options.fpsCap)
|
||||
end },
|
||||
{ label = "CANCEL", cancel = true },
|
||||
}
|
||||
|
||||
|
||||
+93
-27
@@ -67,6 +67,10 @@ local SUBMENU_LABEL = {
|
||||
-- (data/text/common_2.asm), the three lines TossMenu prints in order.
|
||||
local TOSS_HOW_MANY = { "Throw away how", "many?" }
|
||||
|
||||
-- _AskItemMoveText (data/text/common_2.asm:322), printed while wSwitchItem
|
||||
-- holds a row and the cursor is looking for its new home.
|
||||
local ASK_ITEM_MOVE = { "Where should this", "be moved to?" }
|
||||
|
||||
-- _YouDontHaveAMonText and .AnEggCantHoldAnItemText, GiveItem's two refusals.
|
||||
local NO_POKEMON = { "You don't have a", "#MON!" }
|
||||
local EGG_CANT_HOLD = { "An EGG can't hold", "an item." }
|
||||
@@ -127,6 +131,9 @@ function PackMenu.new(game, opts)
|
||||
self.game = game
|
||||
self.save = opts.save or (game and game.save)
|
||||
self.items = opts.items or (game and game.data and game.data.items)
|
||||
-- Bag.order / Bag.move read `.items` off an injectable data table, and the
|
||||
-- one this screen draws from is not always the Data singleton.
|
||||
self.bagData = { items = self.items }
|
||||
self.world = opts.world or (game and game.world)
|
||||
self.onChoose = opts.onChoose
|
||||
self.onClose = opts.onClose
|
||||
@@ -207,7 +214,13 @@ end
|
||||
function PackMenu:rebuild()
|
||||
local pocket = self:pocket().id
|
||||
local rows = {}
|
||||
for itemId, raw in pairs((self.save and self.save.inventory) or {}) do
|
||||
local save = self.save
|
||||
local inventory = (save and save.inventory) or {}
|
||||
-- Row order IS wBagItems' order, which is what SELECT rewrites.
|
||||
local order = (save and save.inventory)
|
||||
and Bag.order(save, self.bagData) or {}
|
||||
for _, itemId in ipairs(order) do
|
||||
local raw = inventory[itemId]
|
||||
-- A count that is not a number at all (a hand-written save, a mod, an old
|
||||
-- migration) counts as one rather than raising out of the draw.
|
||||
local count = tonumber(raw) or (raw and 1) or 0
|
||||
@@ -219,18 +232,13 @@ function PackMenu:rebuild()
|
||||
name = PackMenu.label(itemId, def),
|
||||
teaches = self:moveLabel(def and def.teaches),
|
||||
tmNumber = def and def.tmNumber,
|
||||
-- KEY_ITEM and TM_HM rows do not show a quantity on the cart.
|
||||
showCount = pocket == "ITEM" or pocket == "BALL",
|
||||
index = def and def.index or math.huge,
|
||||
-- A KEY_ITEM never shows one, and engine/items/tmhm.asm:390 skips the
|
||||
-- count for an HM only -- a TM prints ×NN like any other stack.
|
||||
showCount = pocket == "ITEM" or pocket == "BALL"
|
||||
or (pocket == "TM_HM" and tostring(itemId):sub(1, 3) ~= "HM_"),
|
||||
}
|
||||
end
|
||||
end
|
||||
-- Bag order on the cart is acquisition order; without that recorded, item id
|
||||
-- order is the stable, reproducible choice.
|
||||
table.sort(rows, function(a, b)
|
||||
if a.index ~= b.index then return a.index < b.index end
|
||||
return a.id < b.id
|
||||
end)
|
||||
self.rows = rows
|
||||
self.index = math.min(self.index, #rows + 1)
|
||||
if self.index < 1 then self.index = 1 end
|
||||
@@ -671,6 +679,11 @@ function PackMenu:update(_dt)
|
||||
self:updateQuantity(input)
|
||||
return
|
||||
end
|
||||
-- engine/items/pack.asm:1237 Pack_InterpretJoypad .switching_item
|
||||
if self.switching then
|
||||
self:updateSwitch(input)
|
||||
return
|
||||
end
|
||||
if self.message then
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.message = nil
|
||||
@@ -716,11 +729,52 @@ function PackMenu:update(_dt)
|
||||
end
|
||||
return
|
||||
elseif input:wasPressed("select") then
|
||||
self:registerSelected()
|
||||
self:armSwitch()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- engine/items/pack.asm:1290 Pack_InterpretJoypad .select
|
||||
function PackMenu:armSwitch()
|
||||
if self:isCancel() then return end
|
||||
if not self.rows[self.index] then return end
|
||||
self.switching = self.index
|
||||
self.message = ASK_ITEM_MOVE
|
||||
end
|
||||
|
||||
-- `.switching_item` (engine/items/pack.asm:1297): A or SELECT places, B backs
|
||||
-- out, and left/right cannot leave the pocket mid-move.
|
||||
function PackMenu:updateSwitch(input)
|
||||
if input:wasPressed("up") then
|
||||
self.index = self.index > 1 and self.index - 1 or self:total()
|
||||
self:ensureVisible()
|
||||
elseif input:wasPressed("down") then
|
||||
self.index = self.index < self:total() and self.index + 1 or 1
|
||||
self:ensureVisible()
|
||||
elseif input:wasPressed("a") or input:wasPressed("select") then
|
||||
self:placeSwitch()
|
||||
elseif input:wasPressed("b") then
|
||||
self:endSwitch()
|
||||
end
|
||||
end
|
||||
|
||||
-- engine/items/pack.asm:1307 .place_insert / .end_switch
|
||||
function PackMenu:placeSwitch()
|
||||
local from = self.switching
|
||||
local row = self.rows[from]
|
||||
if row and not self:isCancel() and self.index ~= from then
|
||||
Bag.move(self.save, row.id, self:pocket().id, self.index, self.bagData)
|
||||
self:rebuild()
|
||||
self:storeCursor()
|
||||
end
|
||||
self:endSwitch()
|
||||
end
|
||||
|
||||
function PackMenu:endSwitch()
|
||||
self.switching = nil
|
||||
self.message = nil
|
||||
end
|
||||
|
||||
-- VerticalMenu over the submenu rows: up/down wrap, A picks, B is the carry
|
||||
-- that ExitMenu answers with (`ret c`), which is QUIT by another name.
|
||||
function PackMenu:updateSubmenu(input)
|
||||
@@ -777,12 +831,10 @@ function PackMenu:updateConfirm(input)
|
||||
end
|
||||
end
|
||||
|
||||
-- RegisterItem (engine/items/pack.asm), the submenu's SEL row. SELECT on the
|
||||
-- highlighted row reaches the same routine: the cart's SELECT is the bag's own
|
||||
-- item shuffle, which this port does not have, so the button is free and a
|
||||
-- player who knows Gen 1's registration shortcut gets it. World:registerItem
|
||||
-- re-runs CheckSelectableItem's gate (TM/HM and anything CANT_SELECT_F
|
||||
-- refuses), so neither door can register what the cart would not.
|
||||
-- RegisterItem (engine/items/pack.asm), the submenu's SEL row and its ONLY
|
||||
-- door -- the cart's SELECT is the bag's own item shuffle (see armSwitch).
|
||||
-- World:registerItem re-runs CheckSelectableItem's gate (TM/HM and anything
|
||||
-- CANT_SELECT_F refuses), so it cannot register what the cart would not.
|
||||
function PackMenu:registerSelected()
|
||||
if self:isCancel() then return end
|
||||
local row = self.rows[self.index]
|
||||
@@ -828,22 +880,38 @@ end
|
||||
|
||||
-- The list, description and cursor, on top of whatever chrome was drawn.
|
||||
--
|
||||
-- PlaceMenuItemQuantity (engine/menus/menu_2.asm) writes the ×N one row DOWN
|
||||
-- and one column RIGHT of the name -- the quantity is the entry's second line,
|
||||
-- not a right-aligned column, which is why every PACK row is two tiles tall.
|
||||
-- PlaceMenuItemQuantity (engine/menus/menu_2.asm:10) writes the ×N one row
|
||||
-- DOWN and one column RIGHT of the name -- the quantity is the entry's second
|
||||
-- line, not a right-aligned column, which is why every PACK row is two tiles
|
||||
-- tall. Its `lb bc, 1, 2` is a TWO-digit field with the leading digit blanked,
|
||||
-- so the ones digit sits at name + 3 whether the count is 5 or 50.
|
||||
--
|
||||
-- ScrollingMenu_PlaceCursor (engine/menus/scrolling_menu.asm:438) marks the
|
||||
-- row SELECT armed with the hollow ▷ while the solid ▶ goes on looking.
|
||||
function PackMenu:drawList(listX, listY)
|
||||
for row = 1, VISIBLE_ROWS do
|
||||
local i = row + self.scroll
|
||||
local ty = listY + (row - 1) * LIST_SPACING
|
||||
if i <= #self.rows then
|
||||
local entry = self.rows[i]
|
||||
if i == self.index then Chrome.cursor(listX - 1, ty) end
|
||||
if i == self.index then
|
||||
Chrome.cursor(listX - 1, ty)
|
||||
elseif i == self.switching then
|
||||
Chrome.cursor(listX - 1, ty, true)
|
||||
end
|
||||
Chrome.print(entry.name, listX, ty)
|
||||
if entry.teaches then
|
||||
-- The TM pocket puts the move the TM teaches on that second line.
|
||||
-- The TM pocket puts the move the TM teaches on that second line, and
|
||||
-- its count at listX + 9 (engine/items/tmhm.asm:392) -- on the LABEL's
|
||||
-- line here, since the move name owns the one below it.
|
||||
Chrome.print(entry.teaches, listX + 1, ty + 1)
|
||||
if entry.showCount then
|
||||
Chrome.print("\xc3\x97" .. Chrome.number(entry.count, 2),
|
||||
listX + 9, ty)
|
||||
end
|
||||
elseif entry.showCount then
|
||||
Chrome.print("\xc3\x97" .. tostring(entry.count), listX + 1, ty + 1)
|
||||
Chrome.print("\xc3\x97" .. Chrome.number(entry.count, 2),
|
||||
listX + 1, ty + 1)
|
||||
end
|
||||
elseif i == self:total() then
|
||||
if i == self.index then Chrome.cursor(listX - 1, ty) end
|
||||
@@ -897,12 +965,10 @@ function PackMenu:drawSubmenu()
|
||||
end
|
||||
end
|
||||
|
||||
-- TossItem_MenuHeader is `menu_coords 15, 9, SCREEN_WIDTH - 1, TEXTBOX_Y - 1`
|
||||
-- with NoPriceToDisplay behind it: a small box in the bottom right holding
|
||||
-- nothing but the count.
|
||||
-- engine/items/buy_sell_toss.asm:133 BuySellToss_UpdateQuantityDisplay
|
||||
function PackMenu:drawQuantity()
|
||||
Chrome.box(15, 9, 5, 3)
|
||||
Chrome.print("\xc3\x97" .. tostring(self.qtyState.qty), 16, 10)
|
||||
Chrome.print("\xc3\x97" .. Chrome.number(self.qtyState.qty, 2, true), 16, 10)
|
||||
end
|
||||
|
||||
-- YesNoBox's own coords, the same box every other Gen 2 screen here draws.
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 Logger = require("src.core.Logger")
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
@@ -668,17 +669,12 @@ end
|
||||
|
||||
-- PlaceStatusString (engine/pokemon/mon_stats.asm): three letters, and a mon
|
||||
-- with no HP reads FNT whatever its status byte says.
|
||||
local STATUS_STRING = {
|
||||
slp = "SLP", psn = "PSN", brn = "BRN", frz = "FRZ", par = "PAR",
|
||||
poison = "PSN", burn = "BRN", freeze = "FRZ", paralysis = "PAR",
|
||||
sleep = "SLP", toxic = "PSN",
|
||||
}
|
||||
|
||||
local function statusString(mon)
|
||||
if (mon.hp or 0) <= 0 then return "FNT" end
|
||||
local status = mon.status
|
||||
if not status then return nil end
|
||||
return STATUS_STRING[tostring(status):lower()]
|
||||
local class = ItemEffects.STATUS_CLASS[tostring(status):lower()]
|
||||
return class and class:upper()
|
||||
end
|
||||
|
||||
-- One list row's strings, exactly what WritePartyMenuTilemap's quality
|
||||
|
||||
+127
-2
@@ -24,6 +24,8 @@ local Chrome = require("src.ui.gen2.Chrome")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Save = require("src.core.gen2.Save")
|
||||
local SaveMenu = require("src.ui.gen2.SaveMenu")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
@@ -36,6 +38,13 @@ local MON_HOLDING_MAIL = {
|
||||
Strings.source("Please remove the\nMAIL."),
|
||||
}
|
||||
|
||||
-- _ChangeBoxSaveText (data/text/common_2.asm:1306) is three lines whose `cont`
|
||||
-- has already scrolled by the time YesNoBox goes up over its last two.
|
||||
local CHANGE_BOX_SAVE = { "#MON BOX, data", "will be saved. OK?" }
|
||||
|
||||
-- YesNoBox's own `lb bc, SCREEN_WIDTH - 6, 7` (home/menu.asm:382-383).
|
||||
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 14, 7, 6, 5
|
||||
|
||||
local PcMenu = {}
|
||||
PcMenu.__index = PcMenu
|
||||
PcMenu.isOpaque = true
|
||||
@@ -90,6 +99,13 @@ function PcMenu.new(game, opts)
|
||||
self.onClose = opts.onClose
|
||||
self.house = opts.house and true or false
|
||||
self.events = opts.events
|
||||
-- The same route the start menu's SAVE row takes (src/core/Game2.lua:435),
|
||||
-- so the save.write veto and the save.writing event fire here too.
|
||||
self.writer = opts.writer
|
||||
or (game and type(game.writeSave) == "function"
|
||||
and function() return game:writeSave() end)
|
||||
or Save.save
|
||||
self.saveExists = opts.saveExists
|
||||
-- The folded MAIL BOX row belongs to the item PC, and the whose-PC menu
|
||||
-- reaches that through <PLAYER>'s PC (src/ui/gen2/ItemPcMenu.lua), so
|
||||
-- BILL's PC shows the cart's own five rows. The bedroom's PC keeps it: the
|
||||
@@ -156,6 +172,91 @@ function PcMenu:close()
|
||||
if self.onClose then self.onClose(self.changedDecorations) end
|
||||
end
|
||||
|
||||
-- engine/pokemon/bills_pc.asm:2403 BillsPC_ChangeBoxSubmenu .Switch
|
||||
-- engine/menus/save.asm:40 ChangeBoxSaveGame
|
||||
function PcMenu:beginChangeBox(index)
|
||||
self.changeBox = index
|
||||
self.savePhase = "confirm"
|
||||
self.saveChoice = 1
|
||||
self.saveTimer = 0
|
||||
self.saved = nil
|
||||
local existed = self.saveExists
|
||||
if existed == nil then existed = Save.exists("gold") end
|
||||
self.existed = existed
|
||||
end
|
||||
|
||||
-- .refused: `pop de / ret`, with wCurBox untouched and the picker still up.
|
||||
function PcMenu:refuseChangeBox()
|
||||
self.savePhase, self.changeBox = nil, nil
|
||||
self.saveTimer = 0
|
||||
end
|
||||
|
||||
function PcMenu:acceptChangeBox()
|
||||
if self.saveChoice == 2 then return self:refuseChangeBox() end
|
||||
if self.savePhase == "confirm" and self.existed then
|
||||
self.savePhase = "overwrite"
|
||||
self.saveChoice = 1
|
||||
return
|
||||
end
|
||||
self.savePhase = "saving"
|
||||
self.saveTimer = 0
|
||||
end
|
||||
|
||||
-- `pop de / ld a, e / ld [wCurBox], a` sits between SaveBox and
|
||||
-- SavingDontTurnOffThePower, so the new index rides the file that is written.
|
||||
function PcMenu:writeChangeBox()
|
||||
Boxes.setCurrent(self.save, self.changeBox)
|
||||
local ok = self.writer(self.save)
|
||||
self.saved = ok and true or false
|
||||
if ok then SaveMenu.playSaveSfx(self.game, SaveMenu.SFX_SAVE) end
|
||||
end
|
||||
|
||||
function PcMenu:savePrompt()
|
||||
if self.savePhase == "overwrite" then return SaveMenu.OVERWRITE_PROMPT end
|
||||
if self.savePhase == "saving" then return SaveMenu.SAVING_PROMPT end
|
||||
if self.savePhase == "done" then
|
||||
if self.saved then
|
||||
local name = (self.save.player and self.save.player.name) or "GOLD"
|
||||
return { name .. " saved", "the game." }
|
||||
end
|
||||
return { "Could not save.", "" }
|
||||
end
|
||||
return CHANGE_BOX_SAVE
|
||||
end
|
||||
|
||||
function PcMenu:updateChangeBox()
|
||||
-- SavingDontTurnOffThePower is DelayFrames, not a prompt: no button does
|
||||
-- anything until the sequence runs out (engine/menus/save.asm:55).
|
||||
if self.savePhase == "saving" then
|
||||
self.saveTimer = self.saveTimer + 1
|
||||
if self.saveTimer >= SaveMenu.SAVING_FRAMES then
|
||||
self:writeChangeBox()
|
||||
self.savePhase = "done"
|
||||
self.saveTimer = 0
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.savePhase == "done" then
|
||||
self.saveTimer = self.saveTimer + 1
|
||||
if self.saveTimer >= SaveMenu.SAVED_FRAMES then
|
||||
self.savePhase, self.changeBox = nil, nil
|
||||
self.picking = false
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local input = self.game and self.game.input
|
||||
if not input then return end
|
||||
if input:wasPressed("up") or input:wasPressed("down") then
|
||||
self.saveChoice = self.saveChoice == 1 and 2 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
self:acceptChangeBox()
|
||||
elseif input:wasPressed("b") then
|
||||
-- B out of a yes/no is NO (InterpretTwoOptionMenu returns carry).
|
||||
self:refuseChangeBox()
|
||||
end
|
||||
end
|
||||
|
||||
function PcMenu:choose()
|
||||
local entry = self.entries[self.index]
|
||||
if not entry then return end
|
||||
@@ -231,6 +332,11 @@ function PcMenu:update(_dt)
|
||||
return
|
||||
end
|
||||
|
||||
if self.savePhase then
|
||||
self:updateChangeBox()
|
||||
return
|
||||
end
|
||||
|
||||
if self.picking then
|
||||
local total = Boxes.NUM_BOXES
|
||||
if input:wasPressed("up") then
|
||||
@@ -238,8 +344,11 @@ function PcMenu:update(_dt)
|
||||
elseif input:wasPressed("down") then
|
||||
self.pickIndex = self.pickIndex < total and self.pickIndex + 1 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
Boxes.setCurrent(self.save, self.pickIndex)
|
||||
self.picking = false
|
||||
if self.pickIndex == (self.save.currentBox or 1) then
|
||||
self.picking = false
|
||||
else
|
||||
self:beginChangeBox(self.pickIndex)
|
||||
end
|
||||
elseif input:wasPressed("b") then
|
||||
self.picking = false
|
||||
end
|
||||
@@ -287,6 +396,22 @@ function PcMenu:drawPanel()
|
||||
("%d/%d"):format(Boxes.count(self.save, i), Boxes.MONS_PER_BOX),
|
||||
18, ty)
|
||||
end
|
||||
if self.savePhase then
|
||||
-- ChangeBoxSaveGame's MenuTextbox, then YesNoBox over the box list.
|
||||
Chrome.box(0, 12, 20, 6)
|
||||
local lines = self:savePrompt()
|
||||
Chrome.print(lines[1] or "", 1, 14)
|
||||
Chrome.print(lines[2] or "", 1, 16)
|
||||
if self.savePhase == "confirm" or self.savePhase == "overwrite" then
|
||||
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
|
||||
Chrome.print("YES", YESNO_X + 2, YESNO_Y + 1)
|
||||
Chrome.print("NO", YESNO_X + 2, YESNO_Y + 3)
|
||||
Chrome.cursor(YESNO_X + 1,
|
||||
YESNO_Y + (self.saveChoice == 1 and 1 or 3))
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
Chrome.box(0, 14, 20, 4)
|
||||
Chrome.print("Which BOX?", 1, 16)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
@@ -2096,10 +2096,23 @@ function Pokegear:mapCursorSprite(x, y)
|
||||
Chrome.cursor(math.floor(x / 8), math.floor(y / 8))
|
||||
end
|
||||
|
||||
-- PokegearRadio_Init's tile $08 at `depixel 4, 10, 4, 4`, three rows deep
|
||||
-- (data/sprite_anims/oam.asm:588), x = knob (pokegear.asm:1355).
|
||||
function Pokegear:drawTuningKnob()
|
||||
self:loadArrowSheet()
|
||||
if not (self.arrow and self.arrow:available()) then return end
|
||||
local row = self:currentStation()
|
||||
local tx = (72 + (row and row.knob or 0)) / 8
|
||||
self.arrow:draw(0x08, tx, 1)
|
||||
self.arrow:draw(0x08, tx, 2)
|
||||
self.arrow:draw(0x08, tx, 3)
|
||||
end
|
||||
|
||||
function Pokegear:drawRadio()
|
||||
self:ensureTuned()
|
||||
self:drawTilemap(self.gfx and self.gfx.cards and self.gfx.cards.radio)
|
||||
self:drawStrip()
|
||||
self:drawTuningKnob()
|
||||
local station = self:currentStation()
|
||||
-- UpdateRadioStation prints the tuned channel's name at (2,9). Dead air
|
||||
-- prints nothing: NoRadioStation clears the box and leaves it clear.
|
||||
|
||||
@@ -54,6 +54,11 @@ local TIME_X, TIME_Y = 13, 8
|
||||
|
||||
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
|
||||
|
||||
-- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and
|
||||
-- SavingDontTurnOffThePower's own line, shared with the PC's CHANGE BOX save.
|
||||
local OVERWRITE_PROMPT = { "There is already a", "save file. Is it" }
|
||||
local SAVING_PROMPT = { "SAVING… DON'T TURN", "OFF THE POWER." }
|
||||
|
||||
function SaveMenu:wantsFillScale() return true end
|
||||
function SaveMenu:drawsWidescreen() return true end
|
||||
|
||||
@@ -75,14 +80,18 @@ function SaveMenu.new(game, opts)
|
||||
return self
|
||||
end
|
||||
|
||||
function SaveMenu:playSfx(id)
|
||||
local data = self.game and self.game.data
|
||||
function SaveMenu.playSaveSfx(game, id)
|
||||
local data = game and game.data
|
||||
local audio = data and data.audio
|
||||
if not (audio and audio.sfxOrder) then return end
|
||||
local name = audio.sfxOrder[id + 1]
|
||||
if name and audio.sfx and audio.sfx[name] then Sound.play(data, name) end
|
||||
end
|
||||
|
||||
function SaveMenu:playSfx(id)
|
||||
SaveMenu.playSaveSfx(self.game, id)
|
||||
end
|
||||
|
||||
function SaveMenu:finish(saved)
|
||||
if self.onDone then self.onDone(saved) end
|
||||
end
|
||||
@@ -162,10 +171,10 @@ function SaveMenu:prompt()
|
||||
if self.phase == "overwrite" then
|
||||
-- AlreadyASaveFileText when the file is this player's; AnotherSaveFileText
|
||||
-- when the ID differs. Only the first can happen here.
|
||||
return { "There is already a", "save file. Is it" }
|
||||
return OVERWRITE_PROMPT
|
||||
end
|
||||
if self.phase == "saving" then
|
||||
return { "SAVING… DON'T TURN", "OFF THE POWER." }
|
||||
return SAVING_PROMPT
|
||||
end
|
||||
if self.phase == "done" then
|
||||
if self.saved then
|
||||
@@ -226,4 +235,10 @@ function SaveMenu:drawWidescreen(winW, winH)
|
||||
G.pop()
|
||||
end
|
||||
|
||||
SaveMenu.SFX_SAVE = SFX_SAVE
|
||||
SaveMenu.SAVING_FRAMES = SAVING_FRAMES
|
||||
SaveMenu.SAVED_FRAMES = SAVED_FRAMES
|
||||
SaveMenu.OVERWRITE_PROMPT = OVERWRITE_PROMPT
|
||||
SaveMenu.SAVING_PROMPT = SAVING_PROMPT
|
||||
|
||||
return SaveMenu
|
||||
|
||||
@@ -63,6 +63,7 @@ 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")
|
||||
@@ -109,15 +110,6 @@ local TYPE_NAMES = {
|
||||
CURSE_TYPE = "???",
|
||||
}
|
||||
|
||||
-- PlaceStatusString (engine/pokemon/mon_stats.asm): three letters, and a mon
|
||||
-- with no HP reads FNT whatever its status byte says. Same table the party
|
||||
-- list uses; both screens call the same routine on the cart.
|
||||
local STATUS_STRING = {
|
||||
slp = "SLP", psn = "PSN", brn = "BRN", frz = "FRZ", par = "PAR",
|
||||
poison = "PSN", burn = "BRN", freeze = "FRZ", paralysis = "PAR",
|
||||
sleep = "SLP", toxic = "PSN",
|
||||
}
|
||||
|
||||
-- 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.
|
||||
@@ -208,11 +200,15 @@ local function levelText(level)
|
||||
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
|
||||
return STATUS_STRING[tostring(status):lower()]
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user