CLOSES #1478, CLOSES #1479, CLOSES #1488, CLOSES #1510, CLOSES #1511, CLOSES #1514, CLOSES #1521, CLOSES #1522, CLOSES #1545, CLOSES #1557, CLOSES #1563, CLOSES #1565, CLOSES #1566, CLOSES #1577, CLOSES #1594, CLOSES #1596, CLOSES #1606, CLOSES #1608

This commit is contained in:
bryanthaboi
2026-08-20 17:30:18 -04:00
parent 0f8f6d0e4f
commit dbecc345e3
53 changed files with 881 additions and 225 deletions
+16
View File
@@ -87,6 +87,22 @@ do
"and no title row: the box carries no header text")
end
-- the box keeps the palette beneath and caps the cursor at wMaxMenuItem
do
local list = newList(6)
eq(list.sgbPalettes, false,
"no SET_PAL_GENERIC: ItemMenuLoop keeps RunDefaultPaletteCommand's "
.. "palette (start_sub_menus.asm:300)")
eq(list.cursorRows, 3,
"wMaxMenuItem 2: three cursor rows (home/list_menu.asm:46-48)")
list.game = { input = { wasPressed = function(_, b) return b == "down" end,
isDown = function() return false end } }
for _ = 1, 3 do list:update(1 / 60) end
eq(list.index, 4, "three downs reach the fourth item")
eq(list.index - list.scroll, 3,
"scrolling instead of dropping the cursor onto the look-ahead row")
end
-- a short list stops at its last name, and the terminator's CANCEL row is
-- what would follow -- never the '▼'
do
@@ -40,6 +40,11 @@ do
T.eq(battle:shrinkOutScale(battle.player), 3 / 7, "wDownscaledMonSize 1 -> 3x3")
battle.shrinkOut.frame = 6
T.eq(battle:shrinkOutScale(battle.player), 3 / 7, "through Delay3")
battle.shrinkOut.frame = 7
T.eq(battle:shrinkOutScale(battle.player), 0,
"then the 7x7 area holds cleared: no full-size flash before the swap")
T.check(battle:shrinkOutScale({}) == nil,
"the swapped-in battler draws normally")
T.check(battle:shrinkOutScale(battle.enemy) == nil,
"AnimateRetreatingPlayerMon is player-side only")
end
@@ -62,6 +67,8 @@ do
T.check(battle:shrinkOutScale(battle.player) == nil, "and no downscale stage")
battle.queue[3].fn()
T.check((battle.picOff or {}).playerMon == nil, "the slot clears afterwards")
T.eq(battle.sendingOut, true,
"and stays hidden until the swap's send-out (#1545)")
end
-- ---------------------------------------------------------------------
@@ -109,4 +116,62 @@ do
T.check(battle.queue[2] and battle.queue[2].fn ~= nil, "which also reprompts")
end
-- ---------------------------------------------------------------------
-- the SHIFT prompt's picker reprompts on a dead or already-out pick too
-- (HasMonFainted's NoWillText, core.asm:1473-1488) (#1608)
-- ---------------------------------------------------------------------
do
local save = SaveData.newGame()
save.player.name = "RED"
save.party = { Pokemon.new(Data, "FIXMON_A", 30),
Pokemon.new(Data, "FIXMON_B", 30) }
save.options = { battleStyle = "shift" }
local game = { data = Data, save = save,
stack = { top = function() return nil end,
push = function() end, pop = function() end } }
local battle = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
battle.participants = {}
battle.buildScreen = function(_, _, opts) return opts end
-- capture the picker opts the SHIFT branch pushes
local captured
Data.screens = Data.screens or {}
Data.screens.PartyMenu = function(_, opts) captured = opts; return {} end
require("src.ui.Screens").invalidate()
battle.enemyParty[1].hp = 0
battle.enemy.mon = battle.enemyParty[1]
battle:enemyMonFainted()
local choiceRow
for _, row in ipairs(battle.queue) do
if row.choice then choiceRow = row end
end
T.check(choiceRow ~= nil, "SHIFT queues the change-POKeMON choice")
choiceRow.choice(true)
T.check(captured ~= nil and captured.forceSwitch == true,
"YES opens the forced party picker")
battle.queue, battle.nextInsert = {}, 0
save.party[2].hp = 0
captured.onSwitch(save.party[2])
T.check(battle.queue[1] and battle.queue[1].text
and battle.queue[1].text:find("no will", 1, true) ~= nil,
"a fainted SHIFT pick prints NoWillText first")
T.check(battle.queue[2] and battle.queue[2].ui ~= nil,
"then the picker goes straight back up, ahead of the send-out")
T.eq(battle.queue[2].ui(), captured, "with the same forced opts")
battle.queue, battle.nextInsert = {}, 0
captured.onSwitch(battle.player.mon)
T.check(battle.queue[1] and battle.queue[1].text
and battle.queue[1].text:find("already out", 1, true) ~= nil,
"an already-out SHIFT pick prints AlreadyOutText")
T.check(battle.queue[2] and battle.queue[2].ui ~= nil, "and reprompts too")
battle.queue, battle.nextInsert = {}, 0
save.party[2].hp = 10
captured.onSwitch(save.party[2])
T.eq(#battle.queue, 0, "a healthy pick queues no reprompt rows")
Data.screens.PartyMenu = nil
require("src.ui.Screens").invalidate()
end
T.finish("retreat animation and switch reprompt (#1563, #1545, #1608)")
+12 -6
View File
@@ -30,7 +30,9 @@ package.loaded["src.core.Sound"] = {
playCry = function() end,
}
package.loaded["src.render.TextBox"] = {
new = function(_, text, done) return { textBox = true, text = text, done = done } end,
new = function(_, text, done, opts)
return { textBox = true, text = text, done = done, opts = opts }
end,
}
-- BagMenu and PartyMenu bind TextBox at require time, so they load against the
-- stub; Screens caches its factory per id and must be told to forget.
@@ -126,17 +128,21 @@ local function useStone(game)
game.input.pressed = "a"
picker:update(1 / 60)
game.input.pressed = nil
-- IsEvolvingText hands off to the movie when it closes
-- (engine/pokemon/evos_moves.asm:120-134)
-- IsEvolvingText STAYS up; its onShown starts the DelayFrames 50 hold,
-- which then pushes the movie over it (evos_moves.asm:120-134) (#1596)
local intro = game.stack:top()
if not (intro and intro.textBox and intro.done) then
if not (intro and intro.textBox and intro.opts and intro.opts.stay) then
return nil, "the \"is evolving!\" box never opened"
end
if not tostring(intro.text):find("evolving") then
return nil, "the box before the movie is not _IsEvolvingText"
end
game.stack:pop()
intro.done()
intro.opts.stay.onShown()
local hold = game.stack:top()
for _ = 1, 60 do
if pushed then break end
if hold.update then hold.update() end
end
return list
end
@@ -1,5 +1,6 @@
-- The evolution dialogue is the cart's, in the cart's order (#1596):
-- engine/pokemon/evos_moves.asm:120-128, :136-150, :151-153
-- engine/pokemon/evos_moves.asm:120-134 (the clear is rows 0-11 only),
-- :136-150, :151-153
package.path = "./?.lua;./?/init.lua;" .. package.path
@@ -65,10 +66,9 @@ do
"_IsEvolvingText goes up in a real bordered text box first") then
check(textOf(intro):find("is evolving"),
"and it is the cart's line: " .. textOf(intro))
check(intro.auto ~= nil and not intro.auto.wait,
"which waits for no button (IsEvolvingText ends in `done`)")
eq(intro.auto.delay, 50,
"and holds DelayFrames 50 before it clears (evos_moves.asm:122)")
check(intro.stay ~= nil and not intro.stay.prompt,
"which waits for no button (IsEvolvingText ends in `done`) "
.. "and stays up under whatever follows")
end
-- it hands off to the movie on its own, with no input at all
local top
@@ -78,7 +78,15 @@ do
step(game)
end
check(getmetatable(top) == EvolutionState,
"the flash movie opens once that box has cleared itself")
"the flash movie opens once the DelayFrames 50 hold has passed")
local underneath = false
for _, s in ipairs(game.stack.states or {}) do
if s == intro then underneath = true end
end
check(underneath, "the 'is evolving!' box is still on the stack under the "
.. "flash (ClearScreenArea wipes rows 0-11 only, evos_moves.asm:126-128)")
check(not EvolutionState.isOpaque,
"and the flash screen is not opaque, so the box beneath draws")
eq(mon.species, "FIXMON_A",
"and nothing has evolved yet while the box was up")
end
@@ -122,12 +130,4 @@ do
end
end
-- The flash itself carries no text: EvolutionState draws sprites only.
do
local source = assert(io.open("src/ui/EvolutionState.lua")):read("*a")
check(not source:find("Font%.draw"),
"EvolutionState draws no bare text over the flash "
.. "(ClearScreenArea ran before EvolveMon, evos_moves.asm:128)")
end
T.finish()
@@ -35,4 +35,17 @@ local menu = PokedexMenu.new(game, { onClose = function() closed = true end })
menu:close()
check(closed, "close still runs the caller's onClose")
-- Save.newGame seeds the key and Save.validate clamps a hand-edited value
local Save = require("src.core.gen2.Save")
eq(Save.newGame().lastDexMode, "NEW",
"a brand-new save carries the key from the start (wLastDexMode's zero)")
local edited = Save.newGame()
edited.lastDexMode = "SPICY"
Save.validate(edited)
eq(edited.lastDexMode, "NEW", "validate clamps an out-of-range mode to NEW")
local kept = Save.newGame()
kept.lastDexMode = "A-Z"
Save.validate(kept)
eq(kept.lastDexMode, "A-Z", "and keeps a legal one")
T.finish("gen2 pokedex mode persistence bug 1474")
+25 -1
View File
@@ -34,4 +34,28 @@ T.check(not keys["TILESET_KANTO|ROOF_SILVER"],
T.check(keys["TILESET_JOHTO|ROOF_SILVER"],
"while TILESET_JOHTO still takes its group's roof")
print("gen2 Kanto roof gate (#1479): ok")
-- World:atlasFor is the copy the bug was filed against; it has its own
-- ROOF_TILESETS gate, so pin it separately from MapPreview's
local realAssets = package.loaded["src.render.Assets"]
package.loaded["src.render.Assets"] = setmetatable({
image = function() return { setFilter = function() end } end,
resolve = function(path) return path end,
}, { __index = realAssets or { register = function() end } })
package.loaded["src.world.gen2.World"] = nil
local World = require("src.world.gen2.World")
local world = setmetatable({
tilesets = baker.tilesets,
roofs = baker.roofs,
atlasCache = {},
}, { __index = World })
world:atlasFor({ tileset = "TILESET_KANTO", group = 19 })
world:atlasFor({ tileset = "TILESET_JOHTO", group = 19 })
T.check(world.atlasCache["TILESET_KANTO"] ~= nil
and world.atlasCache["TILESET_KANTO|ROOF_SILVER"] == nil,
"World:atlasFor bakes Kanto with no map-group roof (home/map.asm:1738-1749)")
T.check(world.atlasCache["TILESET_JOHTO|ROOF_SILVER"] ~= nil,
"and still roofs TILESET_JOHTO by group")
package.loaded["src.render.Assets"] = realAssets
package.loaded["src.world.gen2.World"] = nil
T.finish("gen2 Kanto roof gate (#1479)")
+51
View File
@@ -0,0 +1,51 @@
-- #1558: menu_gfx.stats -> $31..$41 quads (engine/gfx/load_font.asm:90-95);
-- PAGE_PALETTES is gfx/stats/pages.pal (cgb_layouts.asm:199-212)
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local SummaryMenu = require("src.ui.gen2.SummaryMenu")
-- gfx/stats/pages.pal, RGB 5-bit rows in ROM order: pink, green, blue
local ROM_PALS = {
{ { 31, 31, 31 }, { 31, 19, 31 }, { 31, 15, 31 }, { 0, 0, 0 } },
{ { 31, 31, 31 }, { 21, 31, 14 }, { 17, 31, 0 }, { 0, 0, 0 } },
{ { 31, 31, 31 }, { 17, 31, 31 }, { 17, 31, 31 }, { 0, 0, 0 } },
}
local function up(v) return math.floor(v * 255 / 31 + 0.5) end
for p = 1, 3 do
for c = 1, 4 do
for ch = 1, 3 do
T.eq(SummaryMenu.PAGE_PALETTES[p][c][ch], up(ROM_PALS[p][c][ch]),
("pages.pal palette %d color %d channel %d"):format(p, c, ch))
end
end
end
-- the quads: 17 tiles from $31, one 8x8 cell each off the 136x8 sheet
love.graphics = love.graphics or {}
local realNewQuad = love.graphics.newQuad
love.graphics.newQuad = function(x, y, w, h)
return { x = x, y = y, w = w, h = h }
end
local menu = setmetatable({
menuGfx = { stats = { sheet = "stats", tiles = 17, firstTile = 0x31 } },
picImage = function()
return { getDimensions = function() return 136, 8 end }
end,
}, { __index = SummaryMenu })
local sheet = menu:statsTiles()
if T.check(sheet ~= nil, "menu_gfx.stats builds the tile sheet") then
for id = 0x31, 0x41 do
local q = sheet.quads[id]
T.check(q ~= nil and q.x == (id - 0x31) * 8 and q.w == 8 and q.h == 8,
("tile $%02x maps to sheet cell %d"):format(id, id - 0x31))
end
T.check(sheet.quads[0x42] == nil, "and exactly 17 tiles, no more")
end
-- a cache built before menu_gfx.stats existed keeps the hand-drawn fallback
local old = setmetatable({ menuGfx = {} }, { __index = SummaryMenu })
T.check(old:statsTiles() == nil, "no menu_gfx.stats falls back, not crashes")
love.graphics.newQuad = realNewQuad
T.finish("gen2 stats tiles and page palettes (#1558)")
+7 -2
View File
@@ -93,7 +93,7 @@ do
local money = indexOf(events, "money")
T.check(ret and win and money, "all three win rows are queued: " ..
kinds(events))
T.check(defeated < ret, "BattleText_EnemyWasDefeated comes first")
T.check(defeated < ret, "the last faint event precedes the pic's return")
T.check(ret < win, "the frontpic slides back in before the line")
T.check(win < money, "and PrintWinLossText runs before the payout")
T.eq(winEvent.text, "Th-Thank you!", "the struct's win text is printed")
@@ -153,12 +153,17 @@ do
T.eq(win, "Th-Thank you!", "the struct's win text is decoded")
T.eq(loss, "...Too weak...", "and its loss text with it")
world.vm.winLossArmed = true
world.vm.winTextOverride = "3:4200"
win = World.trainerWinLossText(world)
win, loss = World.trainerWinLossText(world)
T.eq(win, "Scripted win.", "`winlosstext` overwrites the pointer")
-- winlosstext writes BOTH pointers; its 0 loss argument destroyed the
-- struct's value (engine/overworld/scripting.asm:651)
T.eq(loss, nil, "and a 0 loss argument zeroes the loss pointer with it")
world.vm.trainerObject = nil
world.vm.winTextOverride = nil
world.vm.winLossArmed = nil
win, loss = World.trainerWinLossText(world)
T.eq(win, nil, "a battle with no trainer object has no line")
T.eq(loss, nil, "on either side")
@@ -35,13 +35,15 @@ end
local fakeGame = { data = { text = text }, save = { flags = {} } }
local armed
local armed, armedSound
local fakeOw = {
engageTrainer = function(_, _, _, endBattleText) armed = endBattleText end,
engageTrainer = function(_, _, _, endBattleText, _, endBattleSound)
armed, armedSound = endBattleText, endBattleSound
end,
}
local function armedFor(mapId, textId, victoryKey)
armed = nil
armed, armedSound = nil, nil
gyms[mapId].talk[textId](fakeGame, fakeOw, { id = "npc#1" }, function() end)
local labels = victories[victoryKey].dialogue
local want = {}
@@ -63,6 +65,10 @@ local leaders = {
for _, entry in ipairs(leaders) do
local got, want = armedFor(entry[1], entry[2], entry[3])
T.eq(got, want, entry[3] .. " arms its badge line for the battle screen")
-- the dialogue's sound command rides the armed line onto the battle
-- screen too (sound_get_item_1 / sound_get_key_item) (#1606)
T.eq(armedSound, victories[entry[3]].badgeSound,
entry[3] .. " arms its badge jingle beside the line")
end
-- Brock's armed label is one text chain of two text_far pages
@@ -137,3 +143,5 @@ T.check(afterBattleScreen:find(text["_CeruleanGymMistyCascadeBadgeInfoText"],
1, true) ~= nil,
"the TM hand-over still runs on the map")
T.check(rewardGame.save.inventory.CASCADEBADGE == 1, "the badge is still given")
T.finish("gym end battle text (#1606)")
@@ -75,6 +75,25 @@ T.eq(title.menuOpen, true, "the title art stays down until the blink ends")
run(back, 60)
T.eq(title.menuOpen, false, "and comes back once the blink is over")
T.eq(game.stack:top(), title, "leaving the menu lands back on the title")
-- main_menu.asm:70 jumps back to DisplayTitleScreen: the whole cinematic reruns
T.eq(title.phase, "drop", "cancel reruns the boot cinematic from the logo drop")
-- a stranded menuOpen (an onSelect that handed control straight back) may
-- not leave a blank white title behind
title.menuOpen = true
title:update(1 / 60)
T.eq(title.menuOpen, false, "a stranded menuOpen clears on the next update")
-- .finishedWaiting: PlayCry then WaitForSoundToFinish before the white-out
-- (engine/movie/title.asm:241-243)
while title.phase ~= "loop" do title:updateSequence() end
game.input.wasPressed = function(_, b) return b == "start" end
title:update(1 / 60)
game.input.wasPressed = function() return false end
T.eq(title.phase, "exitCry", "START waits out the cry before the white-out")
T.eq(game.stack:top(), title, "no flash is pushed on the cry frame")
run(title, 10)
T.check(game.stack:top() ~= title, "the flash follows once the cry is done")
-- ------------------------------------------- #1511: the intro NAME box
@@ -144,11 +163,20 @@ local function pump(frames)
end
end
pump(400)
T.check(getmetatable(fgame.stack:top()) == Menu, "the preset list opens")
-- _IntroducePlayerText ends in `prompt` (text_2.asm:1730): arrowed A wait
T.check(getmetatable(fgame.stack:top()) ~= Menu,
"the question box waits for A before the name list")
T.check(fgame.stack:top() == flow.holdBox and flow.holdBox.done,
"the typed-out question box is on top, waiting for the press")
fgame.input.wasPressed = function(_, b) return b == "a" end
fgame.stack:top():update(1 / 60)
fgame.input.wasPressed = function() return false end
pump(400)
T.check(getmetatable(fgame.stack:top()) == Menu, "A opens the preset list")
T.eq(fgame.stack.states[2], flow.holdBox,
"IntroducePlayerText's box is still on the stack under the name list")
T.check(flow.holdBox.done and flow.holdBox.stay,
"it is a `stay` box: text_end returns from PrintText without a wait")
T.check(flow.holdBox.stayShown == true,
"prompt-then-hold: the box stays up after the press (home/text.asm:434)")
local preset = fgame.stack:top()
preset.index = 2
@@ -157,7 +185,9 @@ preset:update(1 / 60)
fgame.input.wasPressed = function() return false end
T.eq(fgame.save.player.name, "RED", "picking a preset names the player")
T.eq(flow.holdBox, nil, "and takes the question box down with the list")
run(fgame.stack:top(), 6 * 3)
-- 13-frame ClearScreenArea / DelayFrames beat, then six tiles of slide
-- (oak_speech2.asm:69-78)
run(fgame.stack:top(), 13 + 6 * 3)
T.eq(flow.picSlide, 0, "OakSpeechSlidePicLeft puts the pic back")
T.finish("intro_title_naming_bug1510_1511")
+7 -4
View File
@@ -87,9 +87,12 @@ T.eq(box.text, fakeGame.data.text._AccessedHoFPCText,
box.onDone()
T.same(opened, { "LeaguePC" }, "the Hall of Fame roster screen opens")
-- without the Pokedex the league row still shows (bills_pc.asm checks
-- wNumHoFTeams before EVENT_GOT_POKEDEX)
-- without the Pokedex, .noOaksPC2 skips Oak's PC and the league row alike
-- (bills_pc.asm:48-49, :68-72); only the box height ignores it (:5-7)
fakeGame.save.flags.EVENT_GOT_POKEDEX = nil
local noDex = labels()
T.check(indexOf(noDex, "<PK><MN>LEAGUE"),
"the league row does not depend on EVENT_GOT_POKEDEX")
T.eq(indexOf(noDex, "<PK><MN>LEAGUE"), nil,
"no dex, no PKMN LEAGUE row, HoF teams or not")
T.eq(indexOf(noDex, "PROF.OAK's PC"), nil, "and no PROF.OAK's PC either")
T.finish("pc league row (#1566)")
@@ -159,6 +159,47 @@ do
end
end
-- .useRareCandy: TryEvolvingMon runs over the party list
-- (item_effects.asm:1392-1418) (#1594)
do
local evolveCalls = {}
package.loaded["src.pokemon.Evolution"] = {
pendingFor = function() return "FIXMON_B", { method = "LEVEL" } end,
evolve = function(_, _, to, onDone, via)
evolveCalls[#evolveCalls + 1] = { to = to, onDone = onDone, via = via }
end,
}
package.loaded["src.battle.BattleState"] = {
StatBox = { new = function(_, _, cb) return { statBox = true, cb = cb } end },
}
local game = freshGame(3)
local list = useFromBag(game, nil, "RARE_CANDY")
if check(list ~= nil, "the bag reached the picker (evolution case)") then
local box = game.stack:top()
check(isBox(box), "the level line prints first")
game.stack:pop() -- a real TextBox pops itself before onDone
box.done()
local stat = game.stack:top()
if check(stat and stat.statBox, "then the stat window") then
game.stack:pop() -- as does the stat window before its callback
stat.cb()
eq(#evolveCalls, 1, "the pending evolution starts")
check(inStack(game.stack, isPicker),
"with the party picker STILL up: the evolution prints over it, "
.. "not over the bag list (#1594)")
check(type(evolveCalls[1].onDone) == "function",
"closePicker rides the evolution's completion callback")
evolveCalls[1].onDone()
check(not inStack(game.stack, isPicker),
"and the picker comes down once the evolution flow completes")
check(inStack(game.stack, function(s) return s == list end),
"while the bag list survives (#796)")
end
end
package.loaded["src.pokemon.Evolution"] = nil
package.loaded["src.battle.BattleState"] = nil
end
-- The last candy: the row goes away (RemoveUsedItem empties the slot) and the
-- cursor clamps to a real row -- but the list itself still must not close.
do
+7 -1
View File
@@ -35,6 +35,10 @@ end
T.check(save ~= nil, "the start menu lists SAVE")
save.onSelect()
-- PrintSaveScreenText ends `ld c, 30 / jp DelayFrames`: the bare panel
-- holds 30 frames before the prompt (main_menu.asm:404-405)
T.eq(#game.stack.states, 1, "the panel shows alone first")
for _ = 1, 30 do game.stack:top().update() end
T.eq(#game.stack.states, 2, "the panel and the prompt are two separate states")
local panel, prompt = game.stack.states[1], game.stack.states[2]
T.check(type(panel.draw) == "function" and not panel.isTextBox,
@@ -44,9 +48,11 @@ T.eq(#prompt.pages, 1, "the prompt is one page: no \\f-merged info panel")
T.check(prompt.pages[1][1]:find("Would you like to"),
"the prompt page is WouldYouLikeToSaveText")
-- save.asm:187 hlcoord 0, 7
-- save.asm:188 hlcoord 0, 7
T.eq(prompt.choiceBox.tx, 0, "the save Yes/No box sits at column 0 (left)")
T.eq(prompt.choiceBox.ty, 7, "the save Yes/No box sits at row 7")
T.eq(prompt.choiceBox, require("src.ui.Theme").saveBox,
"the geometry routes through Theme so field.theme can restyle it")
local choice = ChoiceBox.new(game, function() end, { box = prompt.choiceBox })
T.eq(choice.tx, 0, "ChoiceBox honours the save-specific left placement")
+21 -4
View File
@@ -80,10 +80,11 @@ do
battle:performMove(battle.player, battle.enemy, slot)
T.check(indexOf(animRows(battle), "SHRINKING_SQUARE_ANIM") == nil,
"a locked-in Thrash queues no setup animation")
-- .ThrashingAboutCheck falls into PlayerCalcMoveDamage, the same
-- animation pipeline every other move uses (core.asm:3540) (#1577)
T.check(indexOf(animRows(battle), "FIX_THRASH") ~= nil,
"but the move's own animation still plays on a continuation turn")
-- .ThrashingAboutCheck (core.asm:3534-3535, enemy mirror :5909-5910) (#1577)
T.check(indexOf(animRows(battle), "THRASH") ~= nil,
"a continuation turn animates THRASH, not the locked move's own id")
T.check(indexOf(animRows(battle), "FIX_THRASH") == nil,
"so the locked move's own animation does not play")
T.check(hasText(battle, "thrashing about"),
"ThrashingAboutText prints in place of the used-move line")
T.check(not hasText(battle, "used FIX THRASH"),
@@ -128,4 +129,20 @@ do
"and the user is locked into Thrash next turn")
end
-- ---------------------------------------------------------------------
-- JumpMoveEffect (core.asm:3129-3133) before MoveHitTest INVULNERABLE (:3150) (#1565)
do
local battle = newBattle()
battle.queue, battle.nextInsert = {}, 0
battle.enemy.invulnerable = true
battle:performMove(battle.player, battle.enemy, { id = "FIX_THRASH", pp = 20 })
T.check(indexOf(animRows(battle), "SHRINKING_SQUARE_ANIM") ~= nil,
"the setup animation plays against a mid-Fly/Dig target")
T.eq(battle.player.thrashTurns, 2,
"the 2-3 roll commits against a mid-Fly/Dig target")
T.check(battle:menuLockedAction(battle.player) ~= nil,
"and the user is locked into Thrash next turn")
T.check(hasText(battle, "attack missed"), "while the attack itself misses")
end
T.finish("thrash setup animation (#1532)")
+5
View File
@@ -36,5 +36,10 @@ T.eq(boostedText(function(mon, save)
mon.traded = true
mon.otId = save.player.id
end), false, "and a mon traded back to its original trainer loses it (#1488)")
T.eq(boostedText(function(mon)
-- repairTradedOtIds leaves traded mons with otId nil (#1265, #1461)
mon.traded = true
mon.otId = nil
end), true, "a traded mon with no stored OTID keeps the boost (#1488)")
T.finish("traded exp boost is an OTID comparison (#1488)")