mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
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:
@@ -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)")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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)")
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user