Merge pull request #1673 from colsonrice/fix/gen2-battle-text-matches-cart

This commit is contained in:
bryanthaboi
2026-08-21 22:28:42 -04:00
committed by GitHub
3 changed files with 226 additions and 8 deletions
+28 -7
View File
@@ -1254,10 +1254,15 @@ function Battle:dealDamage(attacker, defender, damage, opts)
if opts.critical then
self:emit({ kind = "message", text = "A critical hit!" })
end
-- SuperEffectiveText / NotVeryEffectiveText (data/text/battle.asm:603,608).
-- The cart breaks both across the box's two lines and hyphenates "super-"
-- to do it, and the not-very line ends on the single ellipsis glyph Gold's
-- charmap carries at $75, not three periods.
if opts.effectiveness and opts.effectiveness > 10 then
self:emit({ kind = "message", text = "It's super effective!" })
self:emit({ kind = "message", text = Strings("It's super-\neffective!") })
elseif opts.effectiveness and opts.effectiveness < 10 then
self:emit({ kind = "message", text = "It's not very effective..." })
self:emit({ kind = "message",
text = Strings("It's not very\neffective…") })
end
if endured then
self:emit({ kind = "message",
@@ -1418,7 +1423,9 @@ function Battle:useMove(attacker, defender, moveId)
if not (charging or rampaging or rolling or biding or called) then
if move and (move.pp or 0) <= 0 then
self:emit({ kind = "message", text = "No PP left for this move!" })
-- BattleText_TheresNoPPLeftForThisMove (data/text/battle.asm:315).
self:emit({ kind = "message",
text = Strings("There's no PP left\nfor this move!") })
return
end
if move then move.pp = (move.pp or 1) - 1 end
@@ -1810,8 +1817,12 @@ function Battle:useMove(attacker, defender, moveId)
landed = landed + 1
end
if landed > 1 then
self:emit({ kind = "message",
text = ("Hit %d time(s)!"):format(landed) })
-- PlayerHitTimesText / EnemyHitTimesText (data/text/battle.asm:749,755)
-- are "Hit @ times!". Gen 2 has no singular form of this line, so the
-- plural stands even at one hit rather than the "(s)" this printed.
-- Gen 1 already says it this way (src/battle/EffectRegistry.lua,
-- _HitXTimesText).
self:emit({ kind = "message", text = Strings("Hit %d times!", landed) })
end
-- move_effects/pay_day.asm:13
@@ -2031,8 +2042,11 @@ Battle.MOVE_EFFECTS.EFFECT_PERISH_SONG = function(self)
if mine.perish and theirs.perish then return fail(self) end
if not mine.perish then mine.perish = Effects.PERISH_TURNS end
if not theirs.perish then theirs.perish = Effects.PERISH_TURNS end
-- StartPerishText (data/text/battle.asm:986). What shipped here was a
-- sentence no cart prints; the Gen 2 line names both sides and counts in
-- digits.
self:emit({ kind = "message",
text = "All POKéMON hearing the song will faint in three turns!" })
text = Strings("Both POKéMON will\nfaint in 3 turns!") })
end
-- BattleCommand_Encore: 3-6 turns locked into the move the target last used.
@@ -2162,6 +2176,11 @@ Battle.MOVE_EFFECTS.EFFECT_SPIKES = function(self, attacker, defender)
local side = self:sideOf(defender)
if self.spikes[side] then return fail(self) end
self.spikes[side] = true
-- SpikesText (data/text/battle.asm:974) is three rows, the third scrolled
-- (`cont`) and carrying <TARGET>. The battle message path has no `cont`:
-- src/ui/gen2/BattleState.lua sets self.message straight from the event and
-- printMessage cuts past two rows, so the cart's line cannot be told here
-- yet without the name being dropped on screen. Left as it stands.
self:emit({ kind = "message", text = "Spikes were scattered all around!" })
end
@@ -2399,7 +2418,9 @@ Battle.MOVE_EFFECTS.EFFECT_BEAT_UP = function(self, attacker, defender, def)
{ move = def, moveId = def and def.id })
landed = landed + 1
end
self:emit({ kind = "message", text = ("Hit %d time(s)!"):format(landed) })
-- BattleCommand_EndLoop prints the same line for Beat Up, and Beat Up can
-- land exactly once, which is the case the cart still prints as "times".
self:emit({ kind = "message", text = Strings("Hit %d times!", landed) })
end
-- BattleCommand_Heal (effect_commands.asm:5986): Recover and Rest are both
@@ -0,0 +1,197 @@
-- Five Gen 2 battle messages printed something the cart does not say. Each
-- case below drives a real turn and compares the emitted line against the
-- label in pokegold's data/text/battle.asm:
--
-- SuperEffectiveText :603
-- NotVeryEffectiveText :608
-- BattleText_TheresNoPPLeftForThisMove :315
-- PlayerHitTimesText / EnemyHitTimesText:749, :755
-- StartPerishText :986
--
-- The marker is the one src/import/RomExtractorGen2.lua decodes the cart's own
-- $4e (`line`) into, so a line written here reads exactly as an extracted one
-- would. The tail case pins the other half of that: printMessage draws at
-- most TEXT_ROWS rows and cuts the rest, so a line has to fit two of them.
--
-- luajit tests/engine/gen2_battle_text_matches_cart.lua
--
-- ROM-free: the fixtures below are the extractor's shapes.
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local Battle = require("src.battle.gen2.Battle")
local Mon = require("src.battle.gen2.Mon")
local check = T.check
-- ---------------------------------------------------------------- fixtures
local TYPES = {
NORMAL = { id = "NORMAL", index = 0, category = "physical" },
GROUND = { id = "GROUND", index = 4, category = "physical" },
ROCK = { id = "ROCK", index = 5, category = "physical" },
FIRE = { id = "FIRE", index = 20, category = "special" },
WATER = { id = "WATER", index = 21, category = "special" },
}
local MATCHUPS = {
{ attacker = "NORMAL", defender = "ROCK", multiplier = 5 },
{ attacker = "WATER", defender = "FIRE", multiplier = 20 },
}
local MOVES = {
TACKLE = { id = "TACKLE", name = "TACKLE", power = 35, type = "NORMAL",
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
WATER_GUN = { id = "WATER_GUN", name = "WATER GUN", power = 40,
type = "WATER", accuracy = 100, pp = 25, effect = "EFFECT_NORMAL_HIT" },
DOUBLESLAP = { id = "DOUBLESLAP", name = "DOUBLESLAP", power = 15,
type = "NORMAL", accuracy = 100, pp = 10, effect = "EFFECT_MULTI_HIT" },
PERISH_SONG = { id = "PERISH_SONG", name = "PERISH SONG", power = 0,
type = "NORMAL", accuracy = 100, pp = 5, effect = "EFFECT_PERISH_SONG" },
}
local GROWTH = {
GROWTH_MEDIUM_SLOW = { numerator = 6, denominator = 5, squared = -15,
linear = 100, constant = 140 },
}
local function species(id, index, types)
return { id = id, index = index, name = id,
baseStats = { hp = 50, attack = 60, defense = 50, speed = 50,
specialAttack = 50, specialDefense = 50 },
types = types, catchRate = 255, baseExp = 60,
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 31,
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {} }
end
local POKEMON = {
growthRates = GROWTH,
CYNDAQUIL = species("CYNDAQUIL", 155, { "FIRE", "FIRE" }),
GEODUDE = species("GEODUDE", 74, { "ROCK", "GROUND" }),
}
local DATA = { pokemon = POKEMON, moves = MOVES,
type_chart = { types = TYPES, matchups = MATCHUPS }, items = {} }
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
perfect.hp = Mon.hpDV(perfect)
-- The smallest roll that is neither a critical hit nor a miss.
local function detRandom(n)
if (n or 1) <= 1 then return 0 end
return 1
end
-- One turn of the player's move, returning every message line it printed.
local function linesFrom(playerSpecies, playerMoves, wildSpecies, moveId)
local player = Mon.new(DATA, playerSpecies, 20, { dvs = perfect })
player.moves = playerMoves
local wild = Mon.new(DATA, wildSpecies, 20, { dvs = perfect })
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
local battle = Battle.new({ data = DATA, party = { player }, wild = wild,
random = detRandom })
local said = {}
for _, event in ipairs(battle:takeTurn({ kind = "move", move = moveId })) do
if event.kind == "message" and event.text then
said[#said + 1] = event.text
end
end
return said
end
local function saw(said, text)
for _, line in ipairs(said) do
if line == text then return true end
end
return false
end
local function shown(said)
return "printed: " .. table.concat(said, " | ")
end
-- ---- the two effectiveness lines ------------------------------------------
-- Both break across the box's two lines on the cart, and "super-" is
-- hyphenated to make the break. The not-very line ends on the single
-- ellipsis glyph the Gold charmap carries at $75, not on three periods.
do
local said = linesFrom("CYNDAQUIL",
{ { id = "TACKLE", pp = 35, maxPp = 35 } }, "GEODUDE", "TACKLE")
check(saw(said, "It's not very\neffective…"),
"NotVeryEffectiveText prints as the cart writes it. " .. shown(said))
said = linesFrom("GEODUDE",
{ { id = "WATER_GUN", pp = 25, maxPp = 25 } }, "CYNDAQUIL", "WATER_GUN")
check(saw(said, "It's super-\neffective!"),
"SuperEffectiveText keeps its hyphen and its break. " .. shown(said))
end
-- ---- a move at zero PP ----------------------------------------------------
do
-- a second move with PP left, so the turn refuses the empty one rather
-- than falling through to Struggle
local said = linesFrom("CYNDAQUIL",
{ { id = "TACKLE", pp = 0, maxPp = 35 },
{ id = "WATER_GUN", pp = 25, maxPp = 25 } }, "GEODUDE", "TACKLE")
check(saw(said, "There's no PP left\nfor this move!"),
"the empty-PP refusal is the cart's sentence. " .. shown(said))
end
-- ---- the multi-hit tally --------------------------------------------------
-- "Hit @ times!" has no singular form on the cart, which is why the "(s)"
-- this used to print is not a hedge the game ever makes. Gen 1 says it the
-- same way (src/battle/EffectRegistry.lua, _HitXTimesText).
do
local said = linesFrom("CYNDAQUIL",
{ { id = "DOUBLESLAP", pp = 10, maxPp = 10 } }, "GEODUDE", "DOUBLESLAP")
local tally
for _, line in ipairs(said) do
if line:match("^Hit %d+ times!$") then tally = line end
end
check(tally ~= nil,
"the multi-hit tally reads \"Hit N times!\". " .. shown(said))
check(not table.concat(said, " "):find("time(s)", 1, true),
"and no line hedges the plural with a parenthetical. " .. shown(said))
end
-- ---- Perish Song ----------------------------------------------------------
-- What shipped here was a sentence no cart prints. StartPerishText names
-- both sides and counts in digits.
do
local said = linesFrom("CYNDAQUIL",
{ { id = "PERISH_SONG", pp = 5, maxPp = 5 } }, "GEODUDE", "PERISH_SONG")
check(saw(said, "Both POKéMON will\nfaint in 3 turns!"),
"Perish Song prints StartPerishText. " .. shown(said))
end
-- ---- and every one of them fits the box -----------------------------------
-- printMessage draws Chrome.wrap(self.message, TEXT_WIDTH) and stops at
-- TEXT_ROWS (home/text.asm:143, :397), so a third row is cut rather than
-- spilled. A cart line that needs one, such as SpikesText's `cont` row
-- carrying <TARGET>, cannot be told on this path at all: engine messages set
-- self.message directly (src/ui/gen2/BattleState.lua) instead of going
-- through showPages, so nothing paginates them. Hence Spikes is left alone
-- above, and every line that IS changed is checked to fit here.
do
local Chrome = require("src.ui.gen2.Chrome")
-- TEXT_WIDTH and TEXT_ROWS are file-locals in BattleState, so their values
-- are repeated rather than required.
local BOX_WIDTH, BOX_ROWS = 18, 2
for _, line in ipairs({
"It's super-\neffective!",
"It's not very\neffective…",
"There's no PP left\nfor this move!",
"Hit 3 times!",
"Both POKéMON will\nfaint in 3 turns!",
}) do
local rows = #Chrome.wrap(line, BOX_WIDTH)
check(rows <= BOX_ROWS,
("\"%s\" wraps to %d rows, and the box draws %d")
:format((line:gsub("\n", "\\n")), rows, BOX_ROWS))
end
end
T.finish("gen2 battle text matches the cart")
+1 -1
View File
@@ -175,7 +175,7 @@ do
dealt = event.amount
end
if event.kind == "message"
and event.text == "It's not very effective..." then
and event.text == "It's not very\neffective" then
sawNve = true
end
end