mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
CLOSES #644, CLOSES #703, CLOSES #726, CLOSES #737, CLOSES #750, CLOSES #752, CLOSES #764, CLOSES #765, CLOSES #768, CLOSES #773, CLOSES #774, CLOSES #775, CLOSES #777, CLOSES #780, CLOSES #782
This commit is contained in:
+85
-14
@@ -528,6 +528,10 @@ BattleState.StatBox = StatBox -- the level-up stat window (PrintStatsBox)
|
||||
local function newBattle(game)
|
||||
local self = setmetatable({}, BattleState)
|
||||
self.game = game
|
||||
-- InitBattleVariables (engine/battle/init_battle_variables.asm) zeroes
|
||||
-- wPartyAndBillsPCSavedMenuItem, so entering a battle drops the party
|
||||
-- cursor the field menu has been carrying (src/ui/PartyMenu.lua). #768
|
||||
game.partyMenuSavedIndex = nil
|
||||
self.data = game.data
|
||||
-- ruleset from the merged registry (the requires above are the same
|
||||
-- records on a mod-free boot); an unknown save value falls back to the
|
||||
@@ -644,6 +648,9 @@ function BattleState.newTrainer(game, oppClass, partyIndex)
|
||||
local self = newBattle(game)
|
||||
self.kind = "trainer"
|
||||
self.oppClass = oppClass
|
||||
-- the object_event trainer arg (roster index). computeMusicKind keys
|
||||
-- data/scripts/victories.lua on class#party, so keep it on the battle (#782).
|
||||
self.partyIndex = partyIndex or 1
|
||||
self.trainer = game.data.trainers[oppClass]
|
||||
assert(self.trainer, "unknown trainer class " .. tostring(oppClass))
|
||||
-- pret GetTrainerName_: RIVAL1/2/3 copy wRivalName into wTrainerName
|
||||
@@ -824,6 +831,16 @@ function BattleState:say(text)
|
||||
table.insert(self.queue, { text = text })
|
||||
end
|
||||
|
||||
-- A message whose ROM tail is `text_end` / `done` rather than `prompt`:
|
||||
-- NextTextCommand returns straight out of PrintText on TX_END
|
||||
-- (home/text.asm:328-334) and only TX_PROMPT_BUTTON blinks the arrow and
|
||||
-- runs ManualTextScroll (home/text.asm:434-446), so these pages never wait
|
||||
-- on the player. autoDelay is the frame hold before the queue moves on
|
||||
-- (0 = the next row starts immediately, as PrintText returning does) (#765).
|
||||
function BattleState:sayAuto(text, delay)
|
||||
table.insert(self.queue, { text = text, auto = true, autoDelay = delay or 0 })
|
||||
end
|
||||
|
||||
-- Message that opens YES/NO once typed out, keeping the text visible
|
||||
-- underneath (pokered `done` + TWO_OPTION_MENU / TextBox opts.choice).
|
||||
function BattleState:sayChoice(text, onChoose)
|
||||
@@ -864,6 +881,13 @@ function BattleState:sayNext(text)
|
||||
table.insert(self.queue, self.nextInsert, { text = text })
|
||||
end
|
||||
|
||||
-- sayNext for a page that ends in `text_end` (see sayAuto) (#765)
|
||||
function BattleState:sayNextAuto(text, delay)
|
||||
self.nextInsert = (self.nextInsert or 0) + 1
|
||||
table.insert(self.queue, self.nextInsert,
|
||||
{ text = text, auto = true, autoDelay = delay or 0 })
|
||||
end
|
||||
|
||||
-- insert a UI push right after the current queue item (dex page, the
|
||||
-- level-up stat box -- anything that must keep queue order)
|
||||
function BattleState:uiNext(factory)
|
||||
@@ -1014,6 +1038,8 @@ function BattleState:startMessage(item)
|
||||
self.charIndex = 0
|
||||
self.msgWaiting = nil
|
||||
self.msgPrompt = nil
|
||||
self.msgAutoWait = nil
|
||||
self.msgHold = nil
|
||||
self.scrollPx = nil
|
||||
self:beginMsgLine()
|
||||
end
|
||||
@@ -1268,7 +1294,25 @@ function BattleState:updateQueue()
|
||||
end))
|
||||
return true
|
||||
end
|
||||
if not (item and item.choice) then
|
||||
if item and item.auto then
|
||||
-- No prompt: this page's ROM tail is `text_end`, so PrintText is
|
||||
-- already back with the box still on screen -- pokered's used-move
|
||||
-- line (engine/battle/used_move_text.asm EndUsedMove1Text..
|
||||
-- EndUsedMove5Text) and the item-use line (ItemUseText00,
|
||||
-- engine/items/item_effects.asm) are both of that kind. Only
|
||||
-- TX_PROMPT_BUTTON waits on A/B (home/text.asm:434-446) (#765).
|
||||
self.msgAutoWait = self.msgAutoWait or item.autoDelay or 0
|
||||
if self.msgAutoWait > 0 then
|
||||
self.msgAutoWait = self.msgAutoWait - 1
|
||||
else
|
||||
self.msgAutoWait = nil
|
||||
-- the typed page stays drawn behind whatever runs next (the move
|
||||
-- animation, the ball toss): PrintText leaves the textbox tilemap
|
||||
-- alone and animations only touch sprites (#296)
|
||||
self.msgHold = true
|
||||
self.current = nil
|
||||
end
|
||||
elseif 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
|
||||
@@ -1320,14 +1364,17 @@ end
|
||||
-- gets the final-battle theme
|
||||
function BattleState:computeMusicKind()
|
||||
local isBoss = false
|
||||
if self.kind == "trainer" and self.trainer then
|
||||
if self.kind == "trainer" and self.oppClass then
|
||||
-- wGymLeaderNo is written only by the eight gym scripts
|
||||
-- (scripts/PewterGym.asm .. ViridianGym.asm), so the badge rosters in
|
||||
-- victories.lua are exactly the fights that set it. The lookup must
|
||||
-- include the party index: a class-wide prefix match also caught
|
||||
-- Giovanni's Rocket Hideout (#1) and Silph Co (#2) battles, which never
|
||||
-- touch wGymLeaderNo and take MUSIC_TRAINER_BATTLE like any other
|
||||
-- trainer (#782).
|
||||
local victories = require("data.scripts.victories")
|
||||
for key, reward in pairs(victories) do
|
||||
if reward.badge and key:find(self.trainer.id .. "#", 1, true) == 1 then
|
||||
isBoss = true
|
||||
break
|
||||
end
|
||||
end
|
||||
local reward = victories[self.oppClass .. "#" .. tostring(self.partyIndex or 1)]
|
||||
isBoss = reward ~= nil and reward.badge ~= nil
|
||||
end
|
||||
-- init_battle.asm: challenging a gym leader (wGymLeaderNo, the badge
|
||||
-- fights only -- not Lance or the Champion) bumps the companion's
|
||||
@@ -1575,6 +1622,9 @@ end
|
||||
-- (end_of_battle.asm clears wLowHealthAlarm when a battle ends)
|
||||
function BattleState:exit()
|
||||
require("src.core.Sound").stopLoop("Low_Health_Alarm")
|
||||
-- end_of_battle.asm clears wPartyAndBillsPCSavedMenuItem as well, so the
|
||||
-- field party menu comes back on slot 1 after a battle. #768
|
||||
self.game.partyMenuSavedIndex = nil
|
||||
-- Free this battle's own GPU objects now rather than waiting on a GC
|
||||
-- finalizer: the two full-screen wavy-effect canvases (colorMode) and
|
||||
-- the AnimPlayer's per-instance tilesheet images/quads. The shared
|
||||
@@ -1601,6 +1651,19 @@ local function clearTrapping(battler)
|
||||
battler.trapDamage = nil
|
||||
end
|
||||
|
||||
-- SendOutMon (core.asm:1733-1735) clears both battle cursors, though the
|
||||
-- disassembly only names one of them: `ld hl, wBattleAndStartSavedMenuItem /
|
||||
-- ld [hli], a / ld [hl], a` writes zero to that byte AND to the byte behind
|
||||
-- it, which is wPlayerMoveListIndex (wram.asm:242-244). So every player
|
||||
-- send-out puts the main menu back on FIGHT and the move list back on the
|
||||
-- first slot; the cursors are only remembered across sub-menus of the mon
|
||||
-- that is already out (#737). Enemy send-outs run EnemySendOutFirstMon,
|
||||
-- not SendOutMon, and leave both alone.
|
||||
local function sendOutMonCursors(self)
|
||||
self.menuIndex = 1
|
||||
self.moveIndex = 1
|
||||
end
|
||||
|
||||
-- core.asm:297-300: both sides' FLINCHED bits are cleared as a turn's move
|
||||
-- selection opens, but the clear is skipped for a mon that must recharge or
|
||||
-- is locked into Rage (core.asm:293-295 -- the Hyper Beam flinch-recharge
|
||||
@@ -2084,7 +2147,7 @@ function BattleState:oldManThrow()
|
||||
self.phase = "messages"
|
||||
self.afterQueue = "finish"
|
||||
self.result = "run" -- nothing is kept; wBattleResult only ends the demo
|
||||
self:say(Strings("%s used\nPOKé BALL!", self.demoName or "OLD MAN"))
|
||||
self:sayAuto(Strings("%s used\nPOKé BALL!", self.demoName or "OLD MAN"))
|
||||
self:act(function()
|
||||
require("src.core.Sound").play(self.data, "Ball_Toss")
|
||||
-- ItemUseBall's beat before the toss chain (like throwBall)
|
||||
@@ -2278,6 +2341,7 @@ function BattleState:resolveSwitch(newMon)
|
||||
previous = previous,
|
||||
})
|
||||
self:markParticipant()
|
||||
sendOutMonCursors(self)
|
||||
self.sendingOut = true
|
||||
self:sayNext(self:sendOutText(self.player.name))
|
||||
self:animNext("POOF_ANIM", false)
|
||||
@@ -3252,9 +3316,13 @@ end
|
||||
-- damaging pipeline (EffectRegistry.runDamaging).
|
||||
|
||||
-- Gen 1 status/stat primary effects call PlayCurrentMoveAnimation only
|
||||
-- after they land; these failure texts print with no animation.
|
||||
-- after they land; these failure texts print with no animation. Failures
|
||||
-- whose text is an ordinary sentence rather than one of the shared fail
|
||||
-- lines set msgs.failed instead of relying on this sniffer -- Substitute's
|
||||
-- two failure lines name the move, not the failure (#644).
|
||||
local function primaryEffectFailed(msgs)
|
||||
if not msgs or #msgs == 0 then return true end
|
||||
if msgs.failed then return true end
|
||||
local m = msgs[1]
|
||||
if m == "But, it failed!" or m == "Nothing happened!" then return true end
|
||||
if m:find("didn't affect", 1, true) then return true end
|
||||
@@ -3300,7 +3368,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
||||
|
||||
self.moveAnimRow = nil
|
||||
if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then
|
||||
self:sayNext(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
|
||||
self:sayNextAuto(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
|
||||
-- the move's animation plays right after the announcement; the
|
||||
-- damage path attaches the target's hit blink to this row so the
|
||||
-- blink follows the animation (pokered's order). Mimic is the
|
||||
@@ -3796,6 +3864,7 @@ function BattleState:enemyMonFainted()
|
||||
self.participants = {}
|
||||
self:markParticipant()
|
||||
self.nextInsert = 0
|
||||
sendOutMonCursors(self)
|
||||
self.sendingOut = true
|
||||
self:sayNext(self:sendOutText(self.player.name))
|
||||
self:animNext("POOF_ANIM", false)
|
||||
@@ -3988,6 +4057,7 @@ function BattleState:openReplacementMenu()
|
||||
})
|
||||
self:markParticipant()
|
||||
self.nextInsert = 0
|
||||
sendOutMonCursors(self)
|
||||
self.sendingOut = true
|
||||
self:sayNext(self:sendOutText(self.player.name))
|
||||
self:animNext("POOF_ANIM", false)
|
||||
@@ -4026,7 +4096,7 @@ function BattleState:safariAction(choice)
|
||||
|
||||
if choice == "ball" then
|
||||
st.balls = st.balls - 1
|
||||
self:say(Strings("%s used\nSAFARI BALL!", playerName))
|
||||
self:sayAuto(Strings("%s used\nSAFARI BALL!", playerName))
|
||||
self:act(function()
|
||||
require("src.core.Sound").play(self.data, "Ball_Toss")
|
||||
self.lastBall = "SAFARI_BALL"
|
||||
@@ -4348,7 +4418,7 @@ function BattleState:throwBall(ball)
|
||||
-- "<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(self:romText("_ItemUseText001", "%s used\n%s!", self.game.save.player.name,
|
||||
self:sayAuto(self:romText("_ItemUseText001", "%s used\n%s!", self.game.save.player.name,
|
||||
self.data.items[ball].name))
|
||||
end
|
||||
self:act(function()
|
||||
@@ -5348,7 +5418,8 @@ end
|
||||
function BattleState:drawTextArea()
|
||||
Font.drawBox(0, 12, 20, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
if self.phase == "messages" and (self.current or self.animPlaying) then
|
||||
if self.phase == "messages"
|
||||
and (self.current or self.animPlaying or self.msgHold) then
|
||||
-- during the move animation self.current is nil but shown still holds
|
||||
-- the "used X!" lines; keep drawing them like pokered, whose move
|
||||
-- animations only touch sprites and never the textbox tilemap (#296)
|
||||
|
||||
@@ -259,15 +259,29 @@ MoveEffects.primary = {
|
||||
return { romText(battle.data, "_StatusChangesEliminatedText", "All STATUS changes\nare eliminated!") }
|
||||
end,
|
||||
|
||||
-- substitute.asm reaches its PlayCurrentMoveAnimation / AnimationSubstitute
|
||||
-- Bankswitch only inside the success branch, after `set HAS_SUBSTITUTE_UP`;
|
||||
-- .alreadyHasSubstitute and .notEnoughHP fall straight through to PrintText,
|
||||
-- so both failures print with no animation at all. That is load bearing
|
||||
-- here: the SUBSTITUTE animation opens with SE_SLIDE_MON_OFF, which leaves
|
||||
-- the user's pic hidden (BattleState.lua slideOff end state) until the doll
|
||||
-- is drawn in its place -- and with no substituteHP raised there is no doll,
|
||||
-- so a failed Substitute used to erase the user's sprite for the rest of the
|
||||
-- battle (#644). The failed flag rides the message list so performMove can
|
||||
-- peel the announcement-time anim row without matching on printed text.
|
||||
SUBSTITUTE_EFFECT = function(battle, user)
|
||||
if user.substituteHP then return { romText(battle.data, "_HasSubstituteText", "%s\nhas a SUBSTITUTE!", displayName(user)) } end
|
||||
if user.substituteHP then
|
||||
return { romText(battle.data, "_HasSubstituteText", "%s\nhas a SUBSTITUTE!", displayName(user)),
|
||||
failed = true }
|
||||
end
|
||||
local cost = math.floor(user.mon.stats.hp / 4)
|
||||
-- substitute.asm only fails on subtraction underflow (current HP
|
||||
-- strictly below maxHP/4); at equality the substitute is built and
|
||||
-- the user is left standing on exactly 0 HP (it faints only when
|
||||
-- the engine next checks HP, not here)
|
||||
if user.mon.hp < cost then
|
||||
return { romText(battle.data, "_TooWeakSubstituteText", "Too weak to make\na SUBSTITUTE!") }
|
||||
return { romText(battle.data, "_TooWeakSubstituteText", "Too weak to make\na SUBSTITUTE!"),
|
||||
failed = true }
|
||||
end
|
||||
user.mon.hp = user.mon.hp - cost
|
||||
user.substituteHP = cost + 1
|
||||
|
||||
Reference in New Issue
Block a user