diff --git a/src/battle/gen2/Battle.lua b/src/battle/gen2/Battle.lua index 15abbe14..3705153b 100644 --- a/src/battle/gen2/Battle.lua +++ b/src/battle/gen2/Battle.lua @@ -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 . 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 diff --git a/tests/engine/gen2_battle_text_matches_cart.lua b/tests/engine/gen2_battle_text_matches_cart.lua new file mode 100644 index 00000000..ddef31be --- /dev/null +++ b/tests/engine/gen2_battle_text_matches_cart.lua @@ -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 , 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") diff --git a/tests/gen2_battle_ui_test.lua b/tests/gen2_battle_ui_test.lua index d88c636f..549e84e7 100644 --- a/tests/gen2_battle_ui_test.lua +++ b/tests/gen2_battle_ui_test.lua @@ -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