mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 03:02:39 +02:00
CLOSES #1090, CLOSES #1093, CLOSES #1094, CLOSES #1095, CLOSES #1098, CLOSES #1102, CLOSES #1105, CLOSES #1106, CLOSES #1108, CLOSES #1109, CLOSES #1110, CLOSES #1111, CLOSES #1113, CLOSES #1114, CLOSES #1117, CLOSES #1118, CLOSES #1121, CLOSES #1122, CLOSES #1123, CLOSES #1124, CLOSES #1126, CLOSES #1127, CLOSES #1128, CLOSES #1131, CLOSES #1132, CLOSES #1134, CLOSES #1137, CLOSES #1141
This commit is contained in:
@@ -74,7 +74,10 @@ function ChoiceBox:draw()
|
||||
if r and r.setUIAnchor then
|
||||
r:setUIAnchor(tx * 8, ty * 8, tw * 8, th * 8, self.anchor)
|
||||
end
|
||||
Font.drawBox(tx, ty, tw, th)
|
||||
-- pokegold home/menu.asm YesNoBox: font-page tiles take the screen's own
|
||||
-- BG palette 0 colour 0, same as TextBox.lua's paper fold.
|
||||
local paper = self.game and self.game.textboxPaper and self.game:textboxPaper()
|
||||
Font.drawBox(tx, ty, tw, th, paper)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("YES"), (tx + 2) * 8, (ty + 1) * 8)
|
||||
Font.draw(Strings("NO"), (tx + 2) * 8, (ty + 3) * 8)
|
||||
|
||||
+153
-34
@@ -43,8 +43,9 @@ local BattleState = {}
|
||||
BattleState.__index = BattleState
|
||||
BattleState.isOpaque = true
|
||||
|
||||
-- How long a message stays before the next event runs, in logic steps. The
|
||||
-- cart waits for A on most lines; holding A skips faster, same as text boxes.
|
||||
-- Armed while a battle line waits for PromptButton (home/text.asm). Any
|
||||
-- positive value means "hold until A/B"; the cart never times these out, so
|
||||
-- the victory jingle can keep looping through the post-win prompts.
|
||||
local MESSAGE_FRAMES = 48
|
||||
|
||||
-- home/hm_moves.asm:17-25 IsHMMove's .HMMoves.
|
||||
@@ -573,8 +574,10 @@ function BattleState:drawPic(mon, back)
|
||||
local px, py
|
||||
local boxTiles
|
||||
if back then
|
||||
px = BattleState.PLAYER_PIC_TILE_X * 8
|
||||
py = BattleState.PLAYER_PIC_TILE_Y * 8
|
||||
-- pokegold engine/battle/core.asm:8569: 6x6 box, bottom-aligned/centred
|
||||
local box = BattleState.PLAYER_PIC_TILES * 8
|
||||
px = BattleState.PLAYER_PIC_TILE_X * 8 + math.floor((box - w) / 2)
|
||||
py = BattleState.PLAYER_PIC_TILE_Y * 8 + (box - h)
|
||||
boxTiles = BattleState.PLAYER_PIC_TILES
|
||||
else
|
||||
-- Bottom-aligned and horizontally centred inside the 7x7 box.
|
||||
@@ -881,8 +884,12 @@ function BattleState:startAnim(key, opts)
|
||||
if not self.anims.scripts[key] then return false end
|
||||
-- BattleAnimRunScript's own gate: `bit BATTLE_SCENE, [wOptions]` skips the
|
||||
-- move animation entirely, which is the OPTION screen's BATTLE SCENE row.
|
||||
-- The check only applies to a real move id (wFXAnimID+1 == 0); non-move
|
||||
-- ids (isMove unset here) branch straight to .not_move and always run.
|
||||
local options = self.game and self.game.options
|
||||
if options and options.battleScene == false then return false end
|
||||
if options and options.battleScene == false and opts and opts.isMove then
|
||||
return false
|
||||
end
|
||||
opts = opts or {}
|
||||
local data = (self.game and self.game.data) or {}
|
||||
local audio = data.audio or {}
|
||||
@@ -939,11 +946,38 @@ function BattleState:startAnim(key, opts)
|
||||
return true
|
||||
end
|
||||
|
||||
-- wBattleAfterAnim target for this attacker's turn
|
||||
-- (effect_commands.asm:1963-1972): player swing -> enemy shake, and reverse.
|
||||
function BattleState:afterAnimFor(side)
|
||||
if side == "player" then return "ANIM_ENEMY_DAMAGE" end
|
||||
return "ANIM_PLAYER_DAMAGE"
|
||||
end
|
||||
|
||||
function BattleState:animForMove(moveId, side)
|
||||
local key = self.anims and self.anims.moves and self.anims.moves[moveId]
|
||||
return self:startAnim(key, {
|
||||
local started = self:startAnim(key, {
|
||||
turn = self:turnFor(side), animId = moveId, isMove = true,
|
||||
})
|
||||
if started then
|
||||
-- BattleAnimRunScript (anim_commands.asm:55-72): after the move script
|
||||
-- restores HUDs it immediately runs wBattleAfterAnim (the hit shake).
|
||||
-- Queue it so stepAnim chains without waiting on the next event.
|
||||
self.pendingAfterAnim = { name = self:afterAnimFor(side), side = side }
|
||||
end
|
||||
return started
|
||||
end
|
||||
|
||||
-- Kick off a queued after-anim; returns true when one is now running.
|
||||
function BattleState:startPendingAfterAnim()
|
||||
local pending = self.pendingAfterAnim
|
||||
if not pending then return false end
|
||||
self.pendingAfterAnim = nil
|
||||
if self:animForId(pending.name, pending.side) then
|
||||
-- dealDamage's default ANIM_x_DAMAGE is this same shake; skip it there.
|
||||
self.afterAnimPlayed = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- True while BattleAnimClearHud has that side's HUD blanked.
|
||||
@@ -969,10 +1003,16 @@ function BattleState:stepAnim(input)
|
||||
-- tilemap is whatever they had got to and nothing is latched -- the
|
||||
-- explicit latches (a catch) are the only ones that survive a skip.
|
||||
self.anim = nil
|
||||
-- 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()
|
||||
end
|
||||
if not self.anim:step() then
|
||||
self.anim = nil
|
||||
-- pokegold data/moves/animations.asm .Click: anim_keepsprites means
|
||||
-- the OAM outlives the script, so keep the runner for drawing too.
|
||||
if not self.anim.keepSprites then self.anim = nil end
|
||||
if self:startPendingAfterAnim() then return end
|
||||
return self:endSendOutAnim()
|
||||
end
|
||||
end
|
||||
@@ -1103,7 +1143,14 @@ function BattleState:advanceQueue()
|
||||
-- until the next damage or heal event moved it.
|
||||
local battle = self.battle
|
||||
local mon = battle and battle.party and battle.party[event.index]
|
||||
-- pokegold engine/battle/core.asm:7057-7069: every mon that leveled
|
||||
-- gets the stats box, not just the mon currently on the field.
|
||||
self.pendingStatsMon = mon
|
||||
-- BattleText_StringBuffer1GrewToLevel ends in text_end (battle.asm:336-343),
|
||||
-- and the active mon never even prints it (core.asm:7044-7056 jumps to the
|
||||
-- stats box). Either way there is no PromptButton before the stats box.
|
||||
if mon and mon == battle.player then
|
||||
event.text = nil
|
||||
if self.shownHp then
|
||||
self.shownHp.player = mon.hp or 0
|
||||
if self.hpAnim and self.hpAnim.side == "player" then
|
||||
@@ -1257,7 +1304,19 @@ function BattleState:advanceQueue()
|
||||
end
|
||||
if event.text then
|
||||
self.message = event.text
|
||||
self.messageTimer = MESSAGE_FRAMES
|
||||
-- Lines that must not hold the queue for A/B:
|
||||
-- move UsedMoveText -> text_end, then moveanim
|
||||
-- level GrewToLevel is text_end (battle.asm:336-343), then the stats
|
||||
-- box's WaitPressAorB is the real hold
|
||||
-- experience keeps the wait: _ExpPointsText ends in `prompt`
|
||||
-- (common_1.asm:1660-1665). update() runs stepExpAnim before that wait,
|
||||
-- so the bar crawls under the line and A dismisses it before the battle
|
||||
-- can end.
|
||||
if event.kind == "move" or event.kind == "level" then
|
||||
self.messageTimer = 0
|
||||
else
|
||||
self.messageTimer = MESSAGE_FRAMES
|
||||
end
|
||||
-- BattleStartMessage's own line: the enemy HUD comes up on the step after
|
||||
-- it returns (engine/battle/core.asm:7808-7817), not with it.
|
||||
if event.intro then self.introTextShown = true end
|
||||
@@ -1277,14 +1336,25 @@ function BattleState:advanceQueue()
|
||||
end
|
||||
end
|
||||
-- The move's own animation plays over its "used X!" line, which is where
|
||||
-- PlayBattleAnim sits in the effect command list. A damage event that
|
||||
-- follows gets the shared hit animation instead.
|
||||
-- PlayBattleAnim sits in the effect command list. Its after-anim (the hit
|
||||
-- shake) is chained by animForMove / stepAnim, matching BattleAnimRunScript.
|
||||
-- BattleCommand_MoveAnimNoSub (engine/battle/effect_commands.asm:1958) opens
|
||||
-- with `ld a, [wAttackMissed] / and a / jp nz, BattleCommand_MoveDelay`: a
|
||||
-- move that missed burns the delay and plays nothing. Battle:markMissed sets
|
||||
-- event.missed on every wAttackMissed path.
|
||||
if event.kind == "move" and not event.missed then
|
||||
self:animForMove(event.move, event.side)
|
||||
self.afterAnimPlayed = nil
|
||||
self.pendingAfterAnim = nil
|
||||
if not self:animForMove(event.move, event.side) then
|
||||
-- BATTLE SCENE off skips the move script but still runs wBattleAfterAnim
|
||||
-- (anim_commands.asm:55-72 .disabled fallthrough).
|
||||
local options = self.game and self.game.options
|
||||
if options and options.battleScene == false then
|
||||
if self:animForId(self:afterAnimFor(event.side), event.side) then
|
||||
self.afterAnimPlayed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event.kind == "damage" and event.side then
|
||||
-- ANIM_x_DAMAGE is the MOVE's after-anim (effect_commands.asm:1963-1972),
|
||||
-- so only a move hit gets it; `animMove` is HandleWrap's (core.asm:1198-1203).
|
||||
@@ -1293,10 +1363,25 @@ function BattleState:advanceQueue()
|
||||
if event.animMove then
|
||||
self:animForMove(event.animMove, from)
|
||||
elseif event.anim ~= false then
|
||||
self:animForId(event.anim
|
||||
local hit = event.anim
|
||||
or (event.side == "enemy" and "ANIM_ENEMY_DAMAGE"
|
||||
or "ANIM_PLAYER_DAMAGE"), from)
|
||||
or "ANIM_PLAYER_DAMAGE")
|
||||
-- Already played as the move's after-anim; do not shake twice.
|
||||
if self.afterAnimPlayed
|
||||
and (hit == "ANIM_ENEMY_DAMAGE" or hit == "ANIM_PLAYER_DAMAGE") then
|
||||
self.afterAnimPlayed = nil
|
||||
else
|
||||
self:animForId(hit, from)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Status moves still chain the after-anim but emit no damage event to
|
||||
-- consume the latch; drop it before the next unrelated line.
|
||||
self.afterAnimPlayed = nil
|
||||
end
|
||||
if event.kind == "heal" and event.anim and event.side then
|
||||
-- pokegold engine/battle/core.asm:4074 ItemRecoveryAnim
|
||||
self:animForMove(event.anim, event.side)
|
||||
elseif event.kind == "send" and event.side then
|
||||
-- Every enemy send-out goes through ShowSetEnemyMonAndSendOutAnimation
|
||||
-- (engine/battle/core.asm:3354) -- the faint replacement out of
|
||||
@@ -1593,7 +1678,9 @@ function BattleState:update(_dt)
|
||||
|
||||
-- An animation owns the screen for as long as it runs, exactly the way
|
||||
-- RunBattleAnimScript owns the main loop.
|
||||
if self.anim then
|
||||
-- pokegold data/moves/animations.asm .Click: a finished keepsprites run
|
||||
-- no longer owns the loop, just the OAM the draw path still reads.
|
||||
if self.anim and not (self.anim:done() and self.anim.keepSprites) then
|
||||
self:stepAnim(input)
|
||||
return
|
||||
end
|
||||
@@ -1611,32 +1698,45 @@ function BattleState:update(_dt)
|
||||
if Sound.isPlaying(self.waitSfx) then return end
|
||||
self.waitSfx = nil
|
||||
end
|
||||
-- AnimateExpBar sits right after PrintText Text_MonGainedExpPoint
|
||||
-- (core.asm:6881-6888), with that line still on screen. Run the crawl
|
||||
-- before any PromptButton wait so the bar does not sit frozen until A.
|
||||
if self:stepExpAnim() then return end
|
||||
if self.messageTimer > 0 then
|
||||
if self.tutorial then
|
||||
-- PromptButton really does wait for the button; MESSAGE_FRAMES is this
|
||||
-- screen's shortcut for that, and the tutorial cannot take it. The
|
||||
-- DUDE's A lands on frame 0x51 (DudeAutoInput_A), so a line that timed
|
||||
-- out at 48 would hand his press to whichever screen came next.
|
||||
-- PromptButton waits for the button; the tutorial cannot press it, so
|
||||
-- DudeAutoInput_A (frame 0x51) answers. Never auto-timeout here: a
|
||||
-- 48-frame skip would hand his press to the next screen.
|
||||
self:dudeInput(CatchTutorial.PROMPT_STREAM,
|
||||
"prompt:" .. tostring(self.message))
|
||||
else
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
end
|
||||
-- A is the page-advance, exactly like a text box.
|
||||
-- PromptButton (home/text.asm): A/B pages; no frame countdown.
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
return
|
||||
end
|
||||
-- AnimateExpBar is called from GiveExperiencePoints AFTER
|
||||
-- Text_MonGainedExpPoint has been read (engine/battle/core.asm:6884-6888),
|
||||
-- so the crawl runs with that line still standing and the "grew to level"
|
||||
-- line waits behind it.
|
||||
if self:stepExpAnim() then return end
|
||||
-- pokegold engine/battle/core.asm:7057-7069: the stats box shows once
|
||||
-- the "grew to level" line has finished, held for A/B.
|
||||
if self.pendingStatsMon then
|
||||
self.statsBoxMon = self.pendingStatsMon
|
||||
self.pendingStatsMon = nil
|
||||
self.phase = "stats-box"
|
||||
return
|
||||
end
|
||||
self:advanceQueue()
|
||||
return
|
||||
end
|
||||
|
||||
-- pokegold engine/battle/core.asm:7069 (WaitPressAorB_BlinkCursor).
|
||||
if self.phase == "stats-box" then
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.statsBoxMon = nil
|
||||
self.phase = "resolving"
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- The turn CheckPlayerLockedIn skipped the menu for. No input is read: the
|
||||
-- cart falls straight into ParsePlayerAction, whose .locked_in arm has no
|
||||
-- MoveSelectionScreen in front of it.
|
||||
@@ -1653,6 +1753,8 @@ function BattleState:update(_dt)
|
||||
|
||||
if self.phase == "menu" then
|
||||
-- 2x2 grid: left/right swap the column, up/down the row.
|
||||
-- MenuClickSound / PlayClickSFX (home/menu.asm:746-762): SFX_READ_TEXT_2
|
||||
-- on A/B only, never on D-pad.
|
||||
if input:wasPressed("left") or input:wasPressed("right") then
|
||||
self.menuIndex = self.menuIndex % 2 == 1 and self.menuIndex + 1
|
||||
or self.menuIndex - 1
|
||||
@@ -1660,6 +1762,7 @@ function BattleState:update(_dt)
|
||||
self.menuIndex = self.menuIndex <= 2 and self.menuIndex + 2
|
||||
or self.menuIndex - 2
|
||||
elseif input:wasPressed("a") then
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
local choice = MENU[self.menuIndex]
|
||||
if choice == "FIGHT" then
|
||||
-- `call .CheckPlayerHasUsableMoves / ret z` (engine/battle/core.asm
|
||||
@@ -1710,11 +1813,13 @@ function BattleState:update(_dt)
|
||||
end
|
||||
elseif input:wasPressed("b") then
|
||||
-- B leaves the list, and a mark never survives it
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
self.moveSwapIndex = nil
|
||||
self.phase = "menu"
|
||||
elseif input:wasPressed("a") then
|
||||
-- `xor a / ld [wSwappingMove], a` opens the A arm: choosing a move
|
||||
-- cancels a pending swap rather than performing it
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
self.moveSwapIndex = nil
|
||||
local move = moves[self.moveIndex]
|
||||
if not move then return end
|
||||
@@ -1733,7 +1838,6 @@ function BattleState:update(_dt)
|
||||
-- AskGiveNicknameText ends on `done`, so the line stands while the box is
|
||||
-- up rather than paging away from under it.
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1754,7 +1858,6 @@ function BattleState:update(_dt)
|
||||
-- straight through to the enemy's send-out (engine/battle/core.asm:3305-3310).
|
||||
if self.phase == "ask-shift" then
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1775,7 +1878,6 @@ function BattleState:update(_dt)
|
||||
|
||||
if self.phase == "refuse-shift" then
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1797,7 +1899,6 @@ function BattleState:update(_dt)
|
||||
-- what ForcePickPartyMonInBattle's `jr c, .loop` does with the carry.
|
||||
if self.phase == "refuse-switch" then
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1816,7 +1917,6 @@ function BattleState:update(_dt)
|
||||
|
||||
if self.phase == "refuse-move" then
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1829,7 +1929,6 @@ function BattleState:update(_dt)
|
||||
|
||||
if self.phase == "learn-intro" then
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1843,7 +1942,6 @@ function BattleState:update(_dt)
|
||||
|
||||
if self.phase == "ask-forget" or self.phase == "stop-learning" then
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -1864,7 +1962,6 @@ function BattleState:update(_dt)
|
||||
-- MoveCantForgetHMText holds like any prompt, then `jr .loop` reprints
|
||||
-- MoveAskForgetText over the list (engine/pokemon/learn.asm:193-197).
|
||||
if self.messageTimer > 0 then
|
||||
self.messageTimer = self.messageTimer - 1
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.messageTimer = 0
|
||||
end
|
||||
@@ -3089,9 +3186,31 @@ function BattleState:drawPanel()
|
||||
Chrome.cursor(left + 1, index == 1 and 8 or 10)
|
||||
end
|
||||
end
|
||||
if self.phase == "stats-box" and self.statsBoxMon then
|
||||
self:drawStatsBox(self.statsBoxMon)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- pokegold engine/pokemon/mon_stats.asm:118-124 (PrintTempMonStats.StatNames).
|
||||
local STATS_BOX_ROWS = {
|
||||
{ "ATTACK", "attack" }, { "DEFENSE", "defense" },
|
||||
{ "SPCL.ATK", "specialAttack" }, { "SPCL.DEF", "specialDefense" },
|
||||
{ "SPEED", "speed" },
|
||||
}
|
||||
|
||||
-- pokegold engine/battle/core.asm:7060-7066 (box at hlcoord 9,0, stats at 11,y).
|
||||
function BattleState:drawStatsBox(mon)
|
||||
local stats = mon and mon.stats
|
||||
if not stats then return end
|
||||
Chrome.textbox(9, 0, 9, 10)
|
||||
for i, row in ipairs(STATS_BOX_ROWS) do
|
||||
local ty = 1 + (i - 1) * 2
|
||||
Chrome.print(Strings(row[1]), 11, ty)
|
||||
Chrome.printRight(("%d"):format(stats[row[2]] or 0), 19, ty + 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- The BG layer, plus whatever the animation is doing to it, plus the OBJ
|
||||
-- layer on top. OBJs are not affected by SCX/SCY, which is why they are drawn
|
||||
-- after the scanline blit rather than into the canvas with everything else.
|
||||
|
||||
+54
-20
@@ -43,6 +43,7 @@
|
||||
local Assets = require("src.render.Assets")
|
||||
local Boxes = require("src.core.gen2.Boxes")
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local Font = require("src.render.Font")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
local Palettes = require("src.world.gen2.Palettes")
|
||||
@@ -74,6 +75,14 @@ local PARTY_BOX = 0
|
||||
-- can destroy a mon.
|
||||
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" }
|
||||
|
||||
function BoxMenu:submenuRows()
|
||||
if self.mode == "move" then return MOVE_SUBMENU end
|
||||
return WITHDRAW_SUBMENU
|
||||
end
|
||||
|
||||
-- MovePKMNWithoutMail_InsertMon's .Saving_LeaveOn, printed for 20 frames while
|
||||
-- the mon is written into its new home. It stays up here until a button
|
||||
-- clears it, because it is also the only confirmation the player gets that the
|
||||
@@ -164,14 +173,16 @@ end
|
||||
-- The cart's own prompts (PCString_*): short, because the box they print in
|
||||
-- is one row of 18 columns.
|
||||
function BoxMenu:prompt()
|
||||
if self.mode == "deposit" then return "Deposit which one?" end
|
||||
-- engine/pokemon/bills_pc.asm:356-369: PrepSubmenu places PCString_WhatsUp.
|
||||
if self.phase == "submenu" then return "What's up?" end
|
||||
if self.mode == "move" then
|
||||
-- .Init, .PrepSubmenu and .PrepInsertCursor each place their own string.
|
||||
-- .Init and .PrepInsertCursor each place their own string.
|
||||
if self.phase == "insert" then return "Move to where?" end
|
||||
if self.phase == "submenu" then return "What's up?" end
|
||||
return "Choose a <PK><MN>."
|
||||
end
|
||||
return "Choose a POKéMON."
|
||||
-- PCString_ChooseaPKMN: _DepositPKMN.Init and BillsPC_Withdraw.Init both
|
||||
-- place this exact string (engine/pokemon/bills_pc.asm:2185).
|
||||
return "Choose a <PK><MN>."
|
||||
end
|
||||
|
||||
function BoxMenu:total()
|
||||
@@ -208,21 +219,15 @@ function BoxMenu:act()
|
||||
if self.onClose then self.onClose() end
|
||||
return
|
||||
end
|
||||
-- .a_button: the move screen never acts on the list itself. It checks that
|
||||
-- the row really is a mon and steps to $2, .PrepSubmenu.
|
||||
if self.mode == "move" then
|
||||
-- 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
|
||||
if self.mode == "deposit" then
|
||||
ok, result = Boxes.deposit(self.save, self.index, self.boxIndex)
|
||||
else
|
||||
ok, result = Boxes.withdraw(self.save, self.boxIndex, self.index)
|
||||
end
|
||||
local ok, result = Boxes.deposit(self.save, self.index, self.boxIndex)
|
||||
if not ok then
|
||||
self.message = result
|
||||
return
|
||||
@@ -294,12 +299,28 @@ function BoxMenu:openStats()
|
||||
})
|
||||
end
|
||||
|
||||
-- engine/pokemon/bills_pc.asm:397-411: failed withdraw stays on the submenu.
|
||||
function BoxMenu:doWithdraw()
|
||||
local ok, result = Boxes.withdraw(self.save, self.boxIndex, self.index)
|
||||
if not ok then
|
||||
self.message = result
|
||||
return
|
||||
end
|
||||
self.message = nil
|
||||
self.phase = nil
|
||||
self:clampIndex()
|
||||
end
|
||||
|
||||
function BoxMenu:chooseSubmenu()
|
||||
local row = MOVE_SUBMENU[self.submenuIndex]
|
||||
local row = self:submenuRows()[self.submenuIndex]
|
||||
if row == "MOVE" then
|
||||
self:beginMove()
|
||||
elseif row == "WITHDRAW" then
|
||||
self:doWithdraw()
|
||||
elseif row == "STATS" then
|
||||
self:openStats()
|
||||
elseif row == "RELEASE" then
|
||||
self:askRelease()
|
||||
else
|
||||
-- .Cancel: `ld a, $0 / ld [wJumptableIndex], a`.
|
||||
self.phase = nil
|
||||
@@ -417,11 +438,12 @@ function BoxMenu:update(_dt)
|
||||
|
||||
-- .MoveMonWOMailSubmenu, a VerticalMenu: up/down, A picks, B is its carry.
|
||||
if self.phase == "submenu" then
|
||||
local submenu = self:submenuRows()
|
||||
if input:wasPressed("up") then
|
||||
self.submenuIndex = self.submenuIndex > 1 and self.submenuIndex - 1
|
||||
or #MOVE_SUBMENU
|
||||
or #submenu
|
||||
elseif input:wasPressed("down") then
|
||||
self.submenuIndex = self.submenuIndex < #MOVE_SUBMENU
|
||||
self.submenuIndex = self.submenuIndex < #submenu
|
||||
and self.submenuIndex + 1 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
self:chooseSubmenu()
|
||||
@@ -518,6 +540,7 @@ function BoxMenu:askRelease()
|
||||
return
|
||||
end
|
||||
self.message = name .. " was released."
|
||||
self.phase = nil
|
||||
self:clampIndex()
|
||||
end, { defaultNo = true }))
|
||||
end
|
||||
@@ -644,12 +667,12 @@ end
|
||||
|
||||
-- The PC does not mark the selected row with a ▶: BillsPC_UpdateSelectionCursor
|
||||
-- lays 20 OBJs as a frame *around* the row -- ten tiles wide by two tall, top
|
||||
-- left at pixel (71, 31), stepping 16 pixels per row. Those cursor tiles are
|
||||
-- left at pixel (71, 25), stepping 16 pixels per row. Those cursor tiles are
|
||||
-- not extracted, so the frame is drawn as an outline at exactly those pixels,
|
||||
-- which is what the sprite frame looks like.
|
||||
function BoxMenu:drawSelectionFrame(row)
|
||||
local G = love.graphics
|
||||
local x, y = 71, 31 + (row - 1) * 16
|
||||
local x, y = 71, 25 + (row - 1) * 16
|
||||
G.setColor(0, 0, 0, 1)
|
||||
G.setLineWidth(1)
|
||||
G.rectangle("line", x + 0.5, y + 0.5, 80 - 1, 16 - 1)
|
||||
@@ -679,6 +702,9 @@ function BoxMenu:panelMon()
|
||||
end
|
||||
|
||||
function BoxMenu:drawPanel()
|
||||
-- BillsPC_InitGFX loads FontsBattleExtra once for the whole screen and
|
||||
-- never restores the standard font (engine/pokemon/bills_pc.asm:2169).
|
||||
local wasBattle = Font.useBattleExtra(true)
|
||||
Chrome.clear()
|
||||
|
||||
-- Box name header, then the list box hanging off it. BillsPC_BoxName is a
|
||||
@@ -686,6 +712,11 @@ function BoxMenu:drawPanel()
|
||||
Chrome.box(8, 0, 12, 3)
|
||||
Chrome.print(self:title(), 10, 1)
|
||||
Chrome.box(8, 2, 12, 12)
|
||||
-- BillsPC_RefreshTextboxes overwrites its own top corners with '└'/'┘'
|
||||
-- (engine/pokemon/bills_pc.asm:1204-1211) so the list reads as hanging
|
||||
-- off the name box above it.
|
||||
Font.drawCode(Font.BORDER.bl, 8 * 8, 2 * 8)
|
||||
Font.drawCode(Font.BORDER.br, 19 * 8, 2 * 8)
|
||||
|
||||
local list = self:list()
|
||||
local inserting = self.phase == "insert"
|
||||
@@ -724,7 +755,9 @@ function BoxMenu:drawPanel()
|
||||
self:drawEggPic(mon)
|
||||
else
|
||||
self:drawPic(mon)
|
||||
Chrome.print(":L" .. tostring(mon.level or 1), PIC_X, 12)
|
||||
-- PrintLevel always writes the single bold glyph, not ":L"
|
||||
-- (home/pokemon.asm:178-183).
|
||||
Chrome.print("<LV>" .. tostring(mon.level or 1), PIC_X, 12)
|
||||
if mon.gender == "male" then
|
||||
Chrome.print("\xe2\x99\x82", 5, 12)
|
||||
elseif mon.gender == "female" then
|
||||
@@ -756,13 +789,14 @@ function BoxMenu:drawPanel()
|
||||
-- top spacing puts MOVE at (11,6), one row per two tiles.
|
||||
if self.phase == "submenu" then
|
||||
Chrome.box(9, 4, 11, 10)
|
||||
for i, label in ipairs(MOVE_SUBMENU) do
|
||||
for i, label in ipairs(self:submenuRows()) do
|
||||
local ty = 6 + (i - 1) * 2
|
||||
if i == self.submenuIndex then Chrome.cursor(10, ty) end
|
||||
Chrome.print(label, 11, ty)
|
||||
end
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
Font.useBattleExtra(wasBattle)
|
||||
end
|
||||
|
||||
function BoxMenu:draw()
|
||||
|
||||
@@ -896,7 +896,7 @@ local function printPriceOpaque(amount, ty)
|
||||
end
|
||||
|
||||
function MartMenu:drawBuyList()
|
||||
Chrome.box(LIST_BOX_X, LIST_BOX_Y, LIST_BOX_W, LIST_BOX_H)
|
||||
-- pokegold engine/menus/scrolling_menu.asm _InitScrollingMenu: no border for the buy list
|
||||
for row = 1, VISIBLE_ROWS do
|
||||
local i = row + self.scroll
|
||||
local ty = LIST_Y + (row - 1) * LIST_SPACING
|
||||
|
||||
@@ -86,6 +86,10 @@ local ROWS = {
|
||||
--
|
||||
-- The two volume rows clamp at the ends rather than wrapping, the way
|
||||
-- pokered's text-speed cursor does, so holding left reaches OFF and stays.
|
||||
{ id = "controls", label = "CONTROLS", port = true,
|
||||
activate = function(game)
|
||||
require("src.ui.Screens").push(game, "BindingsMenu")
|
||||
end },
|
||||
{ label = "MUSIC VOL", key = "musicVol", port = true,
|
||||
cycle = function(options, delta)
|
||||
options.musicVol = stepVolume(options.musicVol, delta)
|
||||
|
||||
@@ -188,6 +188,22 @@ function PokedexMenu.new(game, opts)
|
||||
self.dexPalette = gfx.palette
|
||||
end
|
||||
|
||||
-- pokegold engine/pokegear/pokegear.asm Pokedex_GetArea: the AREA page
|
||||
-- draws through the Pokegear's own town-map tiles and TownMapPals, not the
|
||||
-- dex's PokedexLZ sheet.
|
||||
local mapGfx = (opts.menuGfx or data.gen2MenuGfx or {}).pokegear
|
||||
self.mapGfx = mapGfx
|
||||
if mapGfx then
|
||||
self.mapSheet = TileSheet.new({
|
||||
path = mapGfx.tiles, wide = mapGfx.tilesWide or 16, firstTile = 0,
|
||||
paletteFor = function(tile)
|
||||
if not mapGfx.palettes then return nil end
|
||||
if tile >= 0x60 then return mapGfx.palettes[1] end
|
||||
return mapGfx.palettes[(mapGfx.palMap and mapGfx.palMap[tile + 1]) or 1]
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- Pokedex_LoadUnownFont: 27 tiles at vTiles2 tile FIRST_UNOWN_CHAR, live
|
||||
-- only while UNOWN MODE is on screen. It is a sheet rather than a font
|
||||
-- page (see PokedexMenu:unownGlyph), and it draws through the dex palette
|
||||
@@ -793,7 +809,7 @@ end
|
||||
function PokedexMenu:playerLandmark()
|
||||
local save = self.game and self.game.save
|
||||
local mapId = save and save.position and save.position.map
|
||||
local def = mapId and self.data and self.data.maps and self.data.maps[mapId]
|
||||
local def = mapId and self.data and self.data.gen2Maps and self.data.gen2Maps[mapId]
|
||||
return def and def.landmark
|
||||
end
|
||||
|
||||
@@ -803,11 +819,13 @@ end
|
||||
-- substitutes them), and borrowing Pokegear's would freeze the map's ink.
|
||||
function PokedexMenu:drawTilemap(cells)
|
||||
if type(cells) ~= "table" then return end
|
||||
local sheet = self.mapSheet
|
||||
if not sheet then return end
|
||||
local i = 1
|
||||
for ty = 0, Chrome.SCREEN_H - 1 do
|
||||
for tx = 0, Chrome.SCREEN_W - 1 do
|
||||
local id = cells[i]
|
||||
if id then self:tile(id, tx, ty) end
|
||||
if id then sheet:draw(id, tx, ty) end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
@@ -837,7 +855,7 @@ function PokedexMenu:drawArea()
|
||||
-- uses. Without it (a cache imported before the town map was extracted) the
|
||||
-- page still lists the landmark NAMES, which is the information the screen
|
||||
-- exists to convey.
|
||||
local maps = self.gfx and self.gfx.maps
|
||||
local maps = self.mapGfx and self.mapGfx.maps
|
||||
local cells = maps and maps[region]
|
||||
if cells then
|
||||
self:drawTilemap(cells)
|
||||
|
||||
@@ -1537,6 +1537,9 @@ function Pokegear:callContact(id)
|
||||
text = self:phoneText("GearOutOfService") }
|
||||
return
|
||||
end
|
||||
-- pokegold engine/pokegear/pokegear.asm:883-889: SFX_CALL rings before the call connects.
|
||||
local world = self.game and self.game.world
|
||||
if world then world:playSfxNamed("Sfx_Call", 106) end
|
||||
local call = Phone.call(self.save, id, context)
|
||||
local name, className = Phone.contactName(id, self.trainers)
|
||||
call.name, call.className = name, className
|
||||
@@ -1562,6 +1565,11 @@ end
|
||||
|
||||
-- HangUp: the click, the boops, and back to "Whom do you want to call?".
|
||||
function Pokegear:hangUp()
|
||||
-- pokegold engine/phone/phone.asm:517-519: HangUp_Beep plays SFX_HANG_UP.
|
||||
if self.call and self.call.kind ~= "nosignal" then
|
||||
local world = self.game and self.game.world
|
||||
if world then world:playSfxNamed("Sfx_HangUp", 107) end
|
||||
end
|
||||
self.call = nil
|
||||
end
|
||||
|
||||
@@ -1768,14 +1776,13 @@ function Pokegear:loadArrowSheet()
|
||||
self.arrow = false
|
||||
local gfx = self.gfx
|
||||
if gfx and gfx.sprites then
|
||||
self:loadPlayerIcon()
|
||||
self.arrow = TileSheet.new({
|
||||
path = gfx.sprites, wide = gfx.spritesWide or 2, firstTile = 0,
|
||||
-- The icon strip's palette (cream / orange / brown / black), not BG
|
||||
-- palette 0's greys: the arrow is an OBJ and the cart tints it to match
|
||||
-- the card icons it points at. The extract carries the gear's BG
|
||||
-- palettes only, and palMap gives every icon-strip tile this same index,
|
||||
-- so it is the one that reproduces the cart rather than a guess.
|
||||
palette = gfx.palettes and (gfx.palettes[4] or gfx.palettes[1]),
|
||||
-- pokegold data/sprite_anims/oam.asm .OAMData_RedWalk: STILL_CURSOR's
|
||||
-- oamset reuses RED_WALK's OAM data, so this wears PAL_OW_RED.
|
||||
palette = (self.playerIcon and self.playerIcon.objColors)
|
||||
or (gfx.palettes and gfx.palettes[1]),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -538,6 +538,10 @@ function TradeAnimView:drawStats(record, offset)
|
||||
G.push()
|
||||
G.translate(offset, WINDOW_Y)
|
||||
Chrome.textbox(PANEL_X, PANEL_Y, PANEL_INNER_W, PANEL_INNER_H)
|
||||
-- pokegold engine/movie/trade_animation.asm:883-897,925-929: PlaceString
|
||||
-- and PrintNum overwrite the border's own tile at cols 4-12, row 0.
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.rectangle("fill", (PANEL_X + 1) * 8, PANEL_Y * 8, 9 * 8, 8)
|
||||
for _, row in ipairs(TEMPLATE_ROWS) do
|
||||
Chrome.print(row.text, PANEL_X + 1, row.row)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user