mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 16:21:30 +02:00
CLOSES #223, CLOSES #233, CLOSES #236, CLOSES #240, CLOSES #241, CLOSES #249, CLOSES #252, CLOSES #255, CLOSES #257, CLOSES #258, CLOSES #263, CLOSES #265, CLOSES #274, CLOSES #275, CLOSES #276, CLOSES #279, CLOSES #280, CLOSES #282, CLOSES #283, CLOSES #287, CLOSES #291, CLOSES #292, CLOSES #293, CLOSES #301, CLOSES #304, CLOSES #315, CLOSES #316, CLOSES #317, CLOSES #321, CLOSES #322, CLOSES #330
This commit is contained in:
+340
-51
@@ -53,16 +53,22 @@ local BALL_ANIMS = {
|
||||
}
|
||||
|
||||
local imageCache = {}
|
||||
-- The three tables below are keyed by the Image OBJECT, not by a path, and a
|
||||
-- running battle holds the pics it built at enter() (battler.sprite,
|
||||
-- playerBackPic, trainerPic). Weak keys let a dropped pic's row go with it,
|
||||
-- so invalidate() can drop the path cache without orphaning what is on
|
||||
-- screen right now (#316).
|
||||
local WEAK_KEYS = { __mode = "k" }
|
||||
-- fully transparent rows below a pic's content (the extracted 32x32 back
|
||||
-- pics carry baked-in padding); used to sit the pic flush on the text box
|
||||
local imagePadBottom = {}
|
||||
local imagePadBottom = setmetatable({}, WEAK_KEYS)
|
||||
-- fully transparent columns left of a pic's content; at 2x (back pics)
|
||||
-- this is subtracted from hlcoord 1,5 so opaque pixels match hardware,
|
||||
-- where those columns were white-on-white rather than shifted content
|
||||
local imagePadLeft = {}
|
||||
local imagePadLeft = setmetatable({}, WEAK_KEYS)
|
||||
-- image -> { path, pal } so palette-fade variants (see fadeImage) can be
|
||||
-- rebuilt for any battle pic, whatever code loaded it
|
||||
local imageMeta = {}
|
||||
local imageMeta = setmetatable({}, WEAK_KEYS)
|
||||
-- pal = { name, colors } recolors the 4 GB shades like the Super Game Boy.
|
||||
-- trueColor art (14 §the 4-shade contract) opts out of the quantize
|
||||
-- entirely, so its palette variant collapses back onto the plain path.
|
||||
@@ -118,10 +124,19 @@ local function getImage(path, pal, trueColor)
|
||||
return imageCache[key]
|
||||
end
|
||||
|
||||
-- hot reload: the next getImage re-resolves every pic through the asset
|
||||
-- search path and re-measures its ground padding
|
||||
-- Hot reload / COLORS change (PaletteFX.setMode calls this): the next
|
||||
-- getImage re-resolves every pic through the asset search path and
|
||||
-- re-measures its ground padding. ONLY the path->image cache is dropped.
|
||||
-- Wiping the three per-image tables as well orphaned the pics a running
|
||||
-- battle already holds: imagePadBottom went nil, so backPlacement lost the
|
||||
-- four transparent rows it grounds the back pic on and the pic jumped
|
||||
-- pad * 2x = 8px UP the frame COLORS changed (#316), and imageMeta went nil,
|
||||
-- so picImage's forced-mono grayImage (#207), fadeImage's BGP variants and
|
||||
-- imagePathOf's battle_sprite_scales lookup all silently stopped resolving.
|
||||
-- Those rows are weak-keyed, so entries for pics nothing references any more
|
||||
-- are collected on their own rather than leaking.
|
||||
function BattleState.invalidate()
|
||||
imageCache, imagePadBottom, imagePadLeft, imageMeta = {}, {}, {}, {}
|
||||
imageCache = {}
|
||||
end
|
||||
|
||||
Assets.register(BattleState.invalidate)
|
||||
@@ -174,6 +189,25 @@ local function grayImage(img)
|
||||
return getImage(meta.path) or img
|
||||
end
|
||||
|
||||
-- The blacked-out battle screen. HandlePlayerBlackOut (core.asm:1151) runs
|
||||
-- SET_PAL_BATTLE_BLACK, i.e. SetPal_BattleBlack sends PalPacket_Black --
|
||||
-- PAL_BLACK in all four slots of BlkPacket_Battle (engine/gfx/palettes.asm:
|
||||
-- 22-25). The mon pics are drawn OVER the zone pass with their palette
|
||||
-- already baked in, so darkening them means re-baking through PAL_BLACK the
|
||||
-- way fadeImage re-bakes through a BGP permutation (#292). Reads the palette
|
||||
-- out of the active pack, exactly like sgbBattlePals, so the zone pass and
|
||||
-- the pics can never disagree. trueColor art has no DMG shades to remap.
|
||||
local function blackImage(data, img)
|
||||
local meta = imageMeta[img]
|
||||
if not meta or meta.trueColor then return img end
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local pack = PaletteFX.pack(data)
|
||||
local colors = pack and pack.palettes and pack.palettes.BLACK
|
||||
if not colors then return img end
|
||||
local name = PaletteFX.usesGbcPack() and "redpp:BLACK" or "BLACK"
|
||||
return getImage(meta.path, { name = name, colors = colors }) or img
|
||||
end
|
||||
|
||||
-- the asset path a loaded battle image came from (nil for the headless
|
||||
-- stub images), so the battle_sprite_scales registry can be looked up by
|
||||
-- the same path data references
|
||||
@@ -200,6 +234,11 @@ function BattleState:picImage(img)
|
||||
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
|
||||
or PaletteFX.mode == "classic"
|
||||
if self.grayPics or mono then return grayImage(img) end
|
||||
-- SET_PAL_BATTLE_BLACK covers every battle palette slot, so the pics go
|
||||
-- dark with the HP bars while the blackout text is up (#292). Below the
|
||||
-- mono check on purpose: the forced-mono modes re-threshold the whole
|
||||
-- frame downstream, and the DMG had no SGB darkening to begin with.
|
||||
if self.blackedOut then return blackImage(self.data, img) end
|
||||
return fadeImage(img, self:activeBgp())
|
||||
end
|
||||
|
||||
@@ -725,6 +764,7 @@ function BattleState:startMessage(item)
|
||||
-- it against self.total); the current line's revealed count is #shown[last]
|
||||
self.charIndex = 0
|
||||
self.msgWaiting = nil
|
||||
self.msgPrompt = nil
|
||||
self.scrollPx = nil
|
||||
self:beginMsgLine()
|
||||
end
|
||||
@@ -930,9 +970,18 @@ function BattleState:updateQueue()
|
||||
end))
|
||||
return true
|
||||
end
|
||||
if not (item and item.choice)
|
||||
and (input:wasPressed("a") or input:wasPressed("b")) then
|
||||
self.current = nil
|
||||
if not (item and item.choice) then
|
||||
-- The page is typed out and waiting on the player: PromptText
|
||||
-- (home/text.asm:209-217) writes '▼' at (18,16) and ManualTextScroll
|
||||
-- blinks it until A/B, so the arrow belongs on a finished page and not
|
||||
-- only on a \v CONT hold (#317). A flag of its own, not msgWaiting:
|
||||
-- that branch above scrolls the NEXT line in, which this page has not
|
||||
-- got, so reusing it would call beginMsgLine on a drained message.
|
||||
self.msgPrompt = true
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.msgPrompt = nil
|
||||
self.current = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
@@ -1021,6 +1070,17 @@ function BattleState:enter()
|
||||
-- sides slide in; the trainer pics stay up until the send-outs
|
||||
self.introSlide = 40
|
||||
self.showEnemyTrainer = self.kind == "trainer" and self.trainerPic ~= nil
|
||||
-- DrawAllPokeballs (common_text.asm:27) puts the party ball rows AND the
|
||||
-- HUD corner/underline tiles under them (PlacePlayerHUDTiles /
|
||||
-- PlaceEnemyHUDTiles, draw_hud_pokeball_gfx.asm:119-165) on screen with
|
||||
-- the intro text; _InitBattleCommon (core.asm:6755-6762) ClearScreenArea's
|
||||
-- both HUD blocks and ClearSprites's the balls the moment that text is
|
||||
-- dismissed. This flag is exactly that window: drawHUDs draws the intro
|
||||
-- chrome while it is up and holds the real enemy HUD back, since a wild
|
||||
-- battle's DrawEnemyHUDAndHPBar only runs after the text (#317). The
|
||||
-- draw is still gated on the slide having landed, so nothing shows while
|
||||
-- the silhouettes are still coming in.
|
||||
self.introBalls = true
|
||||
-- SGB: the player-side battle palette while the back pic is up is
|
||||
-- MonsterPalettes[0] = PAL_MEWMON (wBattleMonSpecies is still 0 when
|
||||
-- the intro's SET_PAL_BATTLE runs -- SetPal_Battle,
|
||||
@@ -1052,12 +1112,27 @@ function BattleState:enter()
|
||||
queueEnemyCry()
|
||||
end
|
||||
self:say(self.introText)
|
||||
-- _InitBattleCommon (core.asm:6755-6762): the instant the intro text is
|
||||
-- dismissed both HUD blocks are cleared and ClearSprites drops the
|
||||
-- pokeball OAM, so the intro chrome never returns for the rest of the
|
||||
-- battle -- not on a switch, and not when the beaten trainer's pic
|
||||
-- scrolls back in (#317, #282)
|
||||
self:act(function() self.introBalls = nil end)
|
||||
if self.kind == "trainer" then
|
||||
-- EnemySendOutFirstMon (core.asm:1308-1310): SlideTrainerPicOffScreen
|
||||
-- walks the foe's pic off the RIGHT edge (hlcoord 18,0, a = 8 tiles,
|
||||
-- one tile every 2 frames) BEFORE TrainerSentOutText -- the pic does
|
||||
-- not blink out under the text (#317)
|
||||
self:act(function() self:slidePic("foe", 0, 64, 4) end)
|
||||
table.insert(self.queue, { wait = 16 })
|
||||
self:act(function()
|
||||
self.showEnemyTrainer = false
|
||||
self:slidePic("foe")
|
||||
end)
|
||||
self:say(Strings("%s sent\nout %s!", self.trainer.name, self.enemy.name))
|
||||
self:act(function()
|
||||
-- EnemySendOutFirstMon (core.asm:1421-1434): after the text the
|
||||
-- pic grows out of the ball (AnimateSendingOutMon), then the cry
|
||||
self.showEnemyTrainer = false
|
||||
self:startGrowIn(self.enemy)
|
||||
end)
|
||||
queueEnemyCry()
|
||||
@@ -1075,13 +1150,20 @@ function BattleState:enter()
|
||||
queueEnemyCry()
|
||||
end
|
||||
if not self.safari and not self.demo then
|
||||
self:say(self:sendOutText(self.player.name))
|
||||
-- Red's pic clears, the POOF plays, then the mon appears with its
|
||||
-- cry (SendOutMon: message -> AnimateSendingOutMon -> PlayCry)
|
||||
-- StartBattle .playerSendOutFirstMon (core.asm:236-240): the back pic
|
||||
-- walks off the LEFT edge (SlideTrainerPicOffScreen, hlcoord 1,5,
|
||||
-- a = 9 tiles, one tile every 2 frames) BEFORE SendOutMon prints
|
||||
-- "Go! X!" -- Red does not simply vanish under the message (#317)
|
||||
self:act(function() self:slidePic("back", 0, -72, 4) end)
|
||||
table.insert(self.queue, { wait = 18 })
|
||||
self:act(function()
|
||||
self.showPlayerBack = false
|
||||
self.sendingOut = true
|
||||
self:slidePic("back")
|
||||
end)
|
||||
self:say(self:sendOutText(self.player.name))
|
||||
-- then the POOF plays and the mon appears with its cry
|
||||
-- (SendOutMon: message -> AnimateSendingOutMon -> PlayCry)
|
||||
table.insert(self.queue, { anim = "POOF_ANIM", attackerIsPlayer = false })
|
||||
self:act(function()
|
||||
self.sendingOut = false
|
||||
@@ -2165,19 +2247,34 @@ end
|
||||
-- battle (EndLowHealthAlarm sets wLowHealthAlarmDisabled, mirrored by
|
||||
-- playVictoryMusic) and every other outcome tears it down in
|
||||
-- end_of_battle.asm -- self.result covers those. The damage drain
|
||||
-- gates the start (the HUD redraw runs after UpdateHPBar finishes),
|
||||
-- but healing out of the red stops it at once (item_effects.asm clears
|
||||
-- the alarm before the bar animates). No alarm before the player HUD
|
||||
-- first draws (send-out), nor in the safari/old-man battles, which
|
||||
-- have no player mon HUD.
|
||||
-- gates the START (the HUD redraw runs after UpdateHPBar finishes) but
|
||||
-- never the stop, and healing out of the red silences it at once
|
||||
-- (item_effects.asm:991-994 clears the alarm before the bar animates).
|
||||
-- No alarm before the player HUD first draws (send-out), nor in the
|
||||
-- safari/old-man battles, which have no player mon HUD.
|
||||
function BattleState:lowHealthAlarmActive()
|
||||
local p = self.player
|
||||
if not p or self.safari or self.demo or self.result
|
||||
or self.lowHealthAlarmDisabled then return false end
|
||||
if self.showPlayerBack or (self.introSlide or 0) > 0 then return false end
|
||||
if p.fainted then return false end
|
||||
-- A siren that is ALREADY sounding follows the drawn bar, not the
|
||||
-- model: wLowHealthAlarm is a latch DrawPlayerHUDAndHPBar only revisits
|
||||
-- once UpdateHPBar2 has finished animating (core.asm:4727-4729 /
|
||||
-- core.asm:4845-4847 both drain first, then jp DrawHUDsAndHPBars), and
|
||||
-- a KO clears it in RemoveFaintedPlayerMon (core.asm:1011-1016), i.e.
|
||||
-- after the bar has drained empty. applyDamage takes the HP off the
|
||||
-- model while the turn is still being queued, so keying a running alarm
|
||||
-- off mon.hp cut it dead for the whole "used X!" line + move animation
|
||||
-- + drain window (#293). max() keeps a heal out of the red silencing
|
||||
-- it on the spot, the way item_effects.asm does.
|
||||
local hp = p.mon.hp
|
||||
if hp <= 0 or p.fainted then return false end
|
||||
if p.shownHP and p.shownHP > hp then return false end -- drain running
|
||||
if self.lowHealthAlarmOn then
|
||||
hp = math.max(hp, shownHP(p))
|
||||
elseif p.shownHP and p.shownHP > hp then
|
||||
return false -- drain running: the HUD redraw has not happened yet
|
||||
end
|
||||
if hp <= 0 then return false end
|
||||
local px = math.max(1, math.floor(hp * 48 / math.max(1, p.mon.stats.hp)))
|
||||
return px < 10
|
||||
end
|
||||
@@ -2193,10 +2290,47 @@ local function stepProgram(prog)
|
||||
return head
|
||||
end
|
||||
|
||||
-- Trainer-pic slides. SlideTrainerPicOffScreen (core.asm:1235) walks a
|
||||
-- trainer pic off its own screen edge one tile every 2 frames (9 tiles left
|
||||
-- for the player back pic, 8 tiles right for the foe), and
|
||||
-- _ScrollTrainerPicAfterBattle (engine/battle/scroll_draw_trainer_pic.asm)
|
||||
-- brings the beaten foe back in from the right one column every 4 frames.
|
||||
-- picOff holds the live programs by slot -- "foe" = the enemy trainer pic,
|
||||
-- "back" = the player's back pic -- as a screen-pixel x offset stepped
|
||||
-- toward `to`; updateFx advances them, drawPicsLayer adds them, and the
|
||||
-- queue rows that start them park a { wait } of the matching length. Call
|
||||
-- with no target to clear a slot (#317, #282).
|
||||
function BattleState:slidePic(slot, from, to, step)
|
||||
self.picOff = self.picOff or {}
|
||||
if to == nil then
|
||||
self.picOff[slot] = nil
|
||||
return
|
||||
end
|
||||
self.picOff[slot] = { x = from or 0, to = to, step = step or 4 }
|
||||
end
|
||||
|
||||
-- the live x offset for a pic slot, 0 when nothing is sliding
|
||||
function BattleState:picOffset(slot)
|
||||
local p = self.picOff and self.picOff[slot]
|
||||
return p and p.x or 0
|
||||
end
|
||||
|
||||
function BattleState:updateFx()
|
||||
if self.introSlide and self.introSlide > 0 then
|
||||
self.introSlide = self.introSlide - 1
|
||||
end
|
||||
-- step each live trainer-pic slide toward its target; a landed program
|
||||
-- holds its offset (the after-battle scroll-in rests two tiles right of
|
||||
-- the battle slot) until its owner clears the slot
|
||||
if self.picOff then
|
||||
for _, p in pairs(self.picOff) do
|
||||
if p.x < p.to then
|
||||
p.x = math.min(p.to, p.x + p.step)
|
||||
elseif p.x > p.to then
|
||||
p.x = math.max(p.to, p.x - p.step)
|
||||
end
|
||||
end
|
||||
end
|
||||
local fx = self.fx
|
||||
if fx then
|
||||
if fx.shake and fx.shake > 0 then fx.shake = fx.shake - 1 end
|
||||
@@ -2285,7 +2419,12 @@ function BattleState:updateFx()
|
||||
-- low-HP alarm (audio/low_health_alarm.asm): the two-tone siren
|
||||
-- loops while the player's bar is red; see lowHealthAlarmActive
|
||||
local Sound = require("src.core.Sound")
|
||||
if self:lowHealthAlarmActive() then
|
||||
-- self.lowHealthAlarmOn mirrors wLowHealthAlarm's bit 7: a latch read
|
||||
-- back inside lowHealthAlarmActive (the RHS sees last frame's value)
|
||||
-- so a sounding siren rides out the next hit's HP drain instead of
|
||||
-- dropping out mid-announcement (#293)
|
||||
self.lowHealthAlarmOn = self:lowHealthAlarmActive()
|
||||
if self.lowHealthAlarmOn then
|
||||
Sound.startLoop(self.data, "Low_Health_Alarm")
|
||||
else
|
||||
Sound.stopLoop("Low_Health_Alarm")
|
||||
@@ -2922,6 +3061,16 @@ function BattleState:enemyMonFainted()
|
||||
local style = tostring((self.game.save.options or {}).battleStyle or "shift")
|
||||
:lower()
|
||||
local partyCount = #self.game.save.party
|
||||
-- ReplaceFaintedEnemyMon (core.asm:892-896): DrawEnemyPokeballs puts the
|
||||
-- foe's party ball row -- and the HUD chrome PlaceEnemyHUDTiles lays
|
||||
-- down under it (draw_hud_pokeball_gfx.asm:9-11, 33-45, 134-141) -- into
|
||||
-- the block FaintEnemyPokemon just cleared, after the exp text and
|
||||
-- BEFORE the next send-out. It survives EnemySendOutFirstMon's
|
||||
-- SlideTrainerPicOffScreen (core.asm:1308-1310, 8 steps x DelayFrames 2)
|
||||
-- so SET style gets the brief flash, and stays up through the whole
|
||||
-- SHIFT prompt below (#283).
|
||||
self:act(function() self.showEnemyBalls = true end)
|
||||
table.insert(self.queue, { wait = 16 })
|
||||
-- SwitchPlayerMon runs AFTER TrainerSentOutText (core.asm:1436-1443)
|
||||
local shiftSwitchMon = nil
|
||||
if style ~= "set" and partyCount > 1 and self.player.mon.hp > 0 then
|
||||
@@ -2957,6 +3106,10 @@ function BattleState:enemyMonFainted()
|
||||
})
|
||||
self.aiUses = self:aiUsesFor()
|
||||
markSeen(self.game, self.enemy.mon.species)
|
||||
-- EnemySendOutFirstMon .next4 (core.asm:1413-1417): ClearSprites and
|
||||
-- the 4x11 ClearScreenArea take the ball row away with the rest of
|
||||
-- the enemy HUD block, right before TrainerSentOutText (#283)
|
||||
self.showEnemyBalls = nil
|
||||
self:markParticipant()
|
||||
-- EnemySendOutFirstMon (core.asm:1413-1435): the enemy HUD area
|
||||
-- clears, TrainerSentOutText prints, THEN the pic appears
|
||||
@@ -2984,6 +3137,20 @@ function BattleState:enemyMonFainted()
|
||||
battle = self, side = self.sides[1],
|
||||
battler = self.player, previous = previous,
|
||||
})
|
||||
-- Taking the SHIFT offer ZEROES wPartyGainExpFlags and
|
||||
-- wPartyFoughtCurrentEnemyFlags before jumping to SwitchPlayerMon
|
||||
-- (EnemySendOutFirstMon tail, core.asm:1436-1443), and SwitchPlayerMon
|
||||
-- then FLAG_SETs only the mon coming in (core.asm:2424-2433). Without
|
||||
-- the reset the mon that was out when the enemy fainted -- marked by
|
||||
-- the send-out act above, which mirrors EnemySendOut's own re-flag
|
||||
-- (core.asm:1276-1289) -- stayed a participant, so the exp divisor in
|
||||
-- enemyMonFainted counted two mons and the switch-in earned half the
|
||||
-- next KO (#275). Voluntary switches (resolveSwitch) and post-faint
|
||||
-- replacements (openReplacementMenu) must NOT do this: pokered's
|
||||
-- party-menu SwitchPlayerMon keeps the outgoing mon flagged, which is
|
||||
-- the deliberate exp-share, and a fainted mon is already dropped by
|
||||
-- onFaint mirroring RemoveFaintedPlayerMon (core.asm:1002-1007).
|
||||
self.participants = {}
|
||||
self:markParticipant()
|
||||
self.nextInsert = 0
|
||||
self.sendingOut = true
|
||||
@@ -2999,16 +3166,39 @@ function BattleState:enemyMonFainted()
|
||||
end
|
||||
local prize = (self.trainer.baseMoney or 0) * self.enemy.mon.level
|
||||
self.game.save.money = self.game.save.money + prize
|
||||
-- the beaten trainer's pic returns for the defeat text (pokered
|
||||
-- DisplayBattleMenu's defeat flow)
|
||||
self:act(function() self.showEnemyTrainer = self.trainerPic ~= nil end)
|
||||
-- TrainerBattleVictory (core.asm:915-933): EndLowHealthAlarm, then
|
||||
-- the victory theme starts BEFORE TrainerDefeatedText and the
|
||||
-- prize money
|
||||
-- TrainerBattleVictory (core.asm:915-949) in order: EndLowHealthAlarm
|
||||
-- and the victory theme, TrainerDefeatedText, ScrollTrainerPicAfterBattle
|
||||
-- (the beaten trainer scrolls back in from the right, one column every 4
|
||||
-- frames, resting two tiles right of the battle slot), DelayFrames 40,
|
||||
-- PrintEndBattleText -- the trainer's OWN loss line, on the battle
|
||||
-- screen -- and only then MoneyForWinningText. Every row rides the
|
||||
-- *Next inserters so it keeps that order behind the running queue item;
|
||||
-- the plain act() the pic used to ride appended to the END of the queue,
|
||||
-- which is why the trainer only flashed up for a frame or two as the
|
||||
-- battle popped and the loss line had to be printed by the overworld
|
||||
-- afterwards, stranding any evolution between two cuts (#282).
|
||||
-- endBattleText is filled in by whoever started the battle
|
||||
-- (OverworldState:engageTrainer in src/world/OverworldController.lua);
|
||||
-- scripted battles that print their own follow-up leave it nil.
|
||||
self:actNext(function() self:playVictoryMusic() end)
|
||||
-- _TrainerDefeatedText: "<PLAYER> defeated\nTRAINER!"
|
||||
self:sayNext(Strings("%s defeated\n%s!", self.game.save.player.name,
|
||||
self.trainer.name))
|
||||
self:actNext(function()
|
||||
self.showEnemyTrainer = self.trainerPic ~= nil
|
||||
if self.showEnemyTrainer then self:slidePic("foe", 64, 16, 2) end
|
||||
end)
|
||||
-- the 24-frame scroll-in plus the DelayFrames 40 that follows it
|
||||
self.nextInsert = (self.nextInsert or 0) + 1
|
||||
table.insert(self.queue, self.nextInsert, { wait = 64 })
|
||||
if self.endBattleText then
|
||||
-- PrintEndBattleText prints one text box; a `para` (\f) inside it
|
||||
-- starts a fresh page, which is a message row of its own here (five
|
||||
-- EndBattleTexts carry one, e.g. _Route9Youngster1EndBattleText)
|
||||
for page in (self.endBattleText .. "\f"):gmatch("(.-)\f") do
|
||||
if page ~= "" then self:sayNext(page) end
|
||||
end
|
||||
end
|
||||
self:sayNext(Strings("%s got ¥%d\nfor winning!", self.game.save.player.name, prize))
|
||||
end
|
||||
self.result = "win"
|
||||
@@ -3076,6 +3266,15 @@ function BattleState:playerMonFainted()
|
||||
-- Oak's Lab starter rival: Rival1WinText only (no blackout lines).
|
||||
-- Any other wipe, including Route 22 RIVAL1, still blacks out.
|
||||
if not BattleState.isOaksLabStarterRival(self) then
|
||||
-- HandlePlayerBlackOut (core.asm:1150-1159): SET_PAL_BATTLE_BLACK runs
|
||||
-- BEFORE PlayerBlackedOutText2, so the enemy pic and both HP bars are
|
||||
-- already dark under the blackout lines (#292). The Oak's Lab starter
|
||||
-- rival returns one line above that call and never darkens. Set here
|
||||
-- rather than queued: this whole function already runs from a queued
|
||||
-- act after "<mon> fainted!" was dismissed, which is where the palette
|
||||
-- command sits. (The Route 22 RIVAL1 wipe darkens one box early, over
|
||||
-- Rival1WinText, which pokered prints just before the same command.)
|
||||
self.blackedOut = true
|
||||
self:sayNext(Strings("%s is out of\nuseable POKéMON!", self.game.save.player.name))
|
||||
self:sayNext(Strings("%s blacked\nout!", self.game.save.player.name))
|
||||
end
|
||||
@@ -3484,13 +3683,41 @@ end
|
||||
|
||||
-- called by BagMenu when a ball is thrown
|
||||
function BattleState:throwBall(ball)
|
||||
self:say(Strings("%s used\n%s!", self.game.save.player.name,
|
||||
self.data.items[ball].name))
|
||||
-- ItemUseBall branches to ThrowBallAtTrainerMon on wIsInBattle != 1
|
||||
-- (item_effects.asm:109-113) BEFORE it reaches `ld hl, ItemUseText00 /
|
||||
-- call PrintText` (:146-147), so a trainer battle never shows the
|
||||
-- "<PLAYER> used <ITEM>!" line (#291). Safari and the old man demo are
|
||||
-- still wIsInBattle == 1, and this port models both as kind == "wild".
|
||||
if self.kind == "wild" then
|
||||
self:say(Strings("%s used\n%s!", self.game.save.player.name,
|
||||
self.data.items[ball].name))
|
||||
end
|
||||
self:act(function()
|
||||
require("src.core.Sound").play(self.data, "Ball_Toss")
|
||||
if self.kind ~= "wild" then
|
||||
self:sayNext(Strings("The TRAINER\nblocked the BALL!"))
|
||||
self:sayNext(Strings("Don't be a thief!"))
|
||||
-- ThrowBallAtTrainerMon (item_effects.asm:2292-2303) still animates the
|
||||
-- toss: MoveAnimation routes TOSS_ANIM to TossBallAnimation, which takes
|
||||
-- its .BlockBall branch in a trainer battle (animations.asm:2582-2585,
|
||||
-- 2629-2637) -- the plain TOSS arc whatever the ball tier, then
|
||||
-- SFX_FAINT_THUD and BLOCKBALL_ANIM, and only then the two texts. The
|
||||
-- ball still counts as used: UseItem_ sets
|
||||
-- wActionResultOrTookBattleTurn = 1 (item_effects.asm:1-3) and this path
|
||||
-- never clears it, so UseBagItem does not fall back to the bag
|
||||
-- (core.asm:2257-2259) and the turn is spent -- the foe moves (#291).
|
||||
local t = self.data.text
|
||||
self:animNext("TOSS_ANIM", true, nil, ball)
|
||||
self:actNext(function()
|
||||
require("src.core.Sound").play(self.data, "Faint_Thud")
|
||||
end)
|
||||
self:animNext("BLOCKBALL_ANIM", true)
|
||||
self:sayNext(t._ThrowBallAtTrainerMonText1
|
||||
or Strings("The trainer\nblocked the BALL!"))
|
||||
self:sayNext(t._ThrowBallAtTrainerMonText2
|
||||
or Strings("Don't be a thief!"))
|
||||
self:act(function()
|
||||
self:executeAction(self.enemy, self.player, self:enemyAction())
|
||||
end)
|
||||
self:act(function() self:endOfTurn() end)
|
||||
return
|
||||
end
|
||||
if self.ghost then
|
||||
@@ -3941,6 +4168,16 @@ function BattleState:sgbBattlePals()
|
||||
local pack = PaletteFX.pack(self.data)
|
||||
local pals = pack and pack.palettes
|
||||
if not pals then return nil end
|
||||
-- HandlePlayerBlackOut (core.asm:1151) runs SET_PAL_BATTLE_BLACK:
|
||||
-- SetPal_BattleBlack sends PalPacket_Black, PAL_BLACK in all four slots of
|
||||
-- BlkPacket_Battle (engine/gfx/palettes.asm:22-25), so every zone of the
|
||||
-- battle screen -- both HP bars and both mon regions -- goes dark behind
|
||||
-- the blackout text. picImage re-bakes the pics through the same palette,
|
||||
-- since those draw over the zone pass rather than through it (#292).
|
||||
if self.blackedOut and pals.BLACK then
|
||||
local b = pals.BLACK
|
||||
return { [0] = b, [1] = b, [2] = b, [3] = b }
|
||||
end
|
||||
local function bar(b)
|
||||
if not b then return pals.GREENBAR end
|
||||
local hp = b.shownHP or b.mon.hp
|
||||
@@ -4180,7 +4417,8 @@ function BattleState:drawPicsLayer(slide, sx, sy)
|
||||
local img = self:picImage(self.trainerPic)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
local ex, ey = enemyPicXY(img, slide, sx, sy)
|
||||
love.graphics.draw(img, ex, ey)
|
||||
-- SlideTrainerPicOffScreen / _ScrollTrainerPicAfterBattle offset (#317)
|
||||
love.graphics.draw(img, ex + self:picOffset("foe"), ey)
|
||||
elseif self.enemy and self.enemy.sprite and not self.enemyHidden
|
||||
and not self.enemySendingOut and not self:fxHidden(self.enemy) then
|
||||
local img = self:picImage(self.enemy.sprite)
|
||||
@@ -4223,7 +4461,9 @@ function BattleState:drawPicsLayer(slide, sx, sy)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
local dx, dy = BattleState.backPlacement(img:getWidth(), img:getHeight(),
|
||||
pad, padL, s)
|
||||
love.graphics.draw(img, dx + slide + sx, dy + sy, 0, s, s)
|
||||
-- picOffset: SlideTrainerPicOffScreen walking the back pic off the left
|
||||
love.graphics.draw(img, dx + slide + sx + self:picOffset("back"),
|
||||
dy + sy, 0, s, s)
|
||||
elseif self.player and self.player.sprite and not hidePlayer
|
||||
and not self.sendingOut and not self:fxHidden(self.player) then
|
||||
local img = self:picImage(self.player.sprite)
|
||||
@@ -4276,9 +4516,13 @@ function BattleState:drawHUDs(slide)
|
||||
local hudShake = (fx and fx.hudShakeX) or 0
|
||||
-- FaintEnemyPokemon clears the enemy HUD area; it stays blank through
|
||||
-- TrainerAboutToUseText until DrawEnemyHUDAndHPBar after the next send-out
|
||||
-- ...and it is not up yet during the intro text either: a wild battle's
|
||||
-- DrawEnemyHUDAndHPBar is called from _InitBattleCommon (core.asm:6763)
|
||||
-- AFTER PrintBeginningBattleText returns, so "Wild X appeared!" shows the
|
||||
-- player's ball row with no enemy HUD beside it (#317)
|
||||
if self.enemy and not self.showEnemyTrainer and not self.enemySendingOut
|
||||
and not self:growInScale(self.enemy) and slide == 0
|
||||
and not self.enemy.fainted then
|
||||
and not self.introBalls and not self.enemy.fainted then
|
||||
-- enemy HUD (DrawEnemyHUDAndHPBar): name row 0, <LV>+level (4,1),
|
||||
-- HP bar (2,2) with the vertical tick at (1,2), underline row 3;
|
||||
-- AnimationShakeEnemyHUD nudges just this block via SCX
|
||||
@@ -4306,28 +4550,60 @@ function BattleState:drawHUDs(slide)
|
||||
end
|
||||
end
|
||||
|
||||
-- ReplaceFaintedEnemyMon -> DrawEnemyPokeballs (core.asm:896,
|
||||
-- draw_hud_pokeball_gfx.asm:9-11 -> SetupEnemyPartyPokeballs :33-45):
|
||||
-- between a KO and the next send-out the foe's ball row sits in the enemy
|
||||
-- HUD block FaintEnemyPokemon cleared, over the chrome PlaceEnemyHUDTiles
|
||||
-- writes with it -- the same $73 (1,2) / $74 (1,3) / $76 run / $78 tiles
|
||||
-- the live HUD draws, minus the HP bar (#283). Its own block rather than
|
||||
-- a third arm of showIntroBalls below: that window is DrawAllPokeballs's
|
||||
-- (#317) and clears for the rest of the battle, this one reopens on every
|
||||
-- enemy faint. wBaseCoordX $48 / wBaseCoordY $20 stepping -8 is screen
|
||||
-- (64,16) leftward, the same row the intro draws.
|
||||
if self.showEnemyBalls and self.enemyParty and slide == 0 then
|
||||
hudTile(0x73, 8, 16)
|
||||
hudTile(0x74, 8, 24)
|
||||
for i = 2, 9 do hudTile(0x76, i * 8, 24) end
|
||||
hudTile(0x78, 80, 24)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
self:drawBallRow(self.enemyParty, 64, 16, -8)
|
||||
end
|
||||
|
||||
-- Safari shows only the ball count; the old man demo shows neither mon
|
||||
if self.safari then
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(("BALLx%2d"):format(self.safari.balls), 88, 72)
|
||||
end
|
||||
-- trainer/link party pokeball rows during the intro
|
||||
-- (SetupPlayerAndEnemyPokeballs, draw_hud_pokeball_gfx.asm)
|
||||
local showIntroBalls = slide == 0 and (
|
||||
(self.kind == "trainer" and (self.showEnemyTrainer or self.showPlayerBack))
|
||||
or (self.kind == "link" and (self.showPlayerBack or self.enemySendingOut))
|
||||
)
|
||||
-- Party pokeball rows and the HUD chrome under them, for exactly the
|
||||
-- window DrawAllPokeballs owns (common_text.asm:27, with the intro text).
|
||||
-- SetupOwnPartyPokeballs runs in EVERY battle, so the player's row belongs
|
||||
-- on the wild intro too -- keying it off the enemy trainer pic meant a
|
||||
-- wild battle never drew one (#317) -- and SetupEnemyPartyPokeballs is
|
||||
-- skipped when wIsInBattle == 1, so only a trainer/link battle gets the
|
||||
-- foe's row. Gating on introBalls rather than on the pics also stops the
|
||||
-- rows coming back when the beaten trainer scrolls in (#282):
|
||||
-- _ScrollTrainerPicAfterBattle redraws tilemap columns and never touches
|
||||
-- OAM, which ClearSprites emptied when the intro text was dismissed.
|
||||
local showIntroBalls = self.introBalls and slide == 0
|
||||
if showIntroBalls then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
if self.enemyParty and (
|
||||
(self.kind == "trainer" and self.showEnemyTrainer)
|
||||
or (self.kind == "link" and self.enemySendingOut)
|
||||
) then
|
||||
if self.enemyParty and (self.kind == "trainer" or self.kind == "link") then
|
||||
-- PlaceEnemyHUDTiles (hlcoord 1,2): $73, then $74 + 8x $76 + $78
|
||||
-- rightward along row 3 (draw_hud_pokeball_gfx.asm:133-165)
|
||||
hudTile(0x73, 8, 16)
|
||||
hudTile(0x74, 8, 24)
|
||||
for i = 2, 9 do hudTile(0x76, i * 8, 24) end
|
||||
hudTile(0x78, 80, 24)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
self:drawBallRow(self.enemyParty, 64, 16, -8)
|
||||
end
|
||||
if self.showPlayerBack then
|
||||
self:drawBallRow(self.playerParty or self.game.save.party, 88, 80, 8)
|
||||
end
|
||||
-- PlacePlayerHUDTiles (hlcoord 18,10): $73, then $77 + 8x $76 + $6F
|
||||
-- LEFTWARD along row 11 (draw_hud_pokeball_gfx.asm:119-131)
|
||||
hudTile(0x73, 144, 80)
|
||||
hudTile(0x77, 144, 88)
|
||||
for i = 10, 17 do hudTile(0x76, i * 8, 88) end
|
||||
hudTile(0x6F, 72, 88)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
self:drawBallRow(self.playerParty or self.game.save.party, 88, 80, 8)
|
||||
end
|
||||
local hidePlayer = self.safari or self.demo
|
||||
if self.player and not hidePlayer and not self.showPlayerBack
|
||||
@@ -4377,9 +4653,10 @@ function BattleState:drawTextArea()
|
||||
Font.drawCode(line[i], 8 + (i - 1) * 8, y)
|
||||
end
|
||||
end
|
||||
-- the blinking down arrow ('▼', glyph $EE) while a \v CONT wait holds the
|
||||
-- box, bottom-right of the box like TextBox / home/text.asm
|
||||
if self.msgWaiting and self.frame % 60 < 30 then
|
||||
-- the blinking down arrow ('▼', glyph $EE) while a \v CONT wait
|
||||
-- (_ContText) or a typed-out page (PromptText) holds the box; both write
|
||||
-- it at (18,16), bottom-right, like TextBox / home/text.asm (#317)
|
||||
if (self.msgWaiting or self.msgPrompt) and self.frame % 60 < 30 then
|
||||
Font.drawCode(0xEE, (0 + 20 - 2) * 8, (12 + 6 - 1) * 8 - 4)
|
||||
end
|
||||
elseif self.phase == "menu" and self.demo then
|
||||
@@ -4419,6 +4696,18 @@ function BattleState:drawTextArea()
|
||||
-- the move box's top border ('─' at (4,12), '┘' at (10,12)).
|
||||
Font.drawBox(0, 8, 11, 5)
|
||||
Font.drawBox(4, 12, 16, 6)
|
||||
-- Those two cells are REPLACED on hardware: MoveSelectionMenu writes them
|
||||
-- straight into the tilemap over the border it just laid down
|
||||
-- (core.asm:2492-2501), and PrintMenuItem's own TextBoxBorder then redraws
|
||||
-- the whole row on top (core.asm:2838-2844). Font.drawCode blits a
|
||||
-- black-on-transparent glyph instead, so the tile underneath survives: the
|
||||
-- move box's '┌' keeps its Poké Ball corner showing through the '─', and
|
||||
-- the '─' the move box drew at (10,12) pokes two dots out from under the
|
||||
-- '┘' (#240). Wipe each cell back to box white first, the way a tilemap
|
||||
-- write does.
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 32, 96, 8, 8)
|
||||
love.graphics.rectangle("fill", 80, 96, 8, 8)
|
||||
Font.drawCode(Font.BORDER.h, 32, 96)
|
||||
Font.drawCode(Font.BORDER.br, 80, 96)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
|
||||
Reference in New Issue
Block a user