mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
199 lines
8.2 KiB
Lua
199 lines
8.2 KiB
Lua
-- A RARE CANDY used from the field bag must leave the bag open (#796).
|
|
--
|
|
-- engine/menus/start_sub_menus.asm, StartMenu_Item / .useOrTossItem sorts the
|
|
-- chosen item with IsInArray against UsableItems_CloseMenu first and
|
|
-- UsableItems_PartyMenu second. RARE_CANDY is in the party-menu array
|
|
-- (data/items/use_party.asm), so it reaches .useItem_partyMenu, which after
|
|
-- `call UseItem` -- when wActionResultOrTookBattleTurn is not $02 --
|
|
-- restores the screen and `jp StartMenu_Item`, i.e. re-enters the item list
|
|
-- instead of CloseStartMenu. StartMenu_Item reloads wBagSavedMenuItem into
|
|
-- wCurrentMenuItem before DisplayListMenuID, so the cursor comes back on the
|
|
-- row you just used: that is what lets a stack of candies be mashed through.
|
|
-- engine/items/item_effects.asm ItemUseVitamin .useRareCandy ends with
|
|
-- RedrawPartyMenu / PrintStatsBox / WaitForTextScrollButtonPress /
|
|
-- LearnMoveFromLevelUp / TryEvolvingMon and `jp RemoveUsedItem` -- it never
|
|
-- whites out and never closes the start menu. Only .useItem_closeMenu items
|
|
-- (UsableItems_CloseMenu: bike, escape rope, rods) jump to CloseStartMenu.
|
|
--
|
|
-- The port popped the bag ListMenu at the head of the leveledTo branch, which
|
|
-- also skipped the "xN" refresh below it, so the level text played over the
|
|
-- overworld and the player was dumped out of the menu per candy.
|
|
-- luajit tests/engine/rare_candy_bag_open_bug796.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
-- Lazily-required inside the use branches, so seeding package.loaded before
|
|
-- the UI modules load is enough to keep the suite silent and love-free.
|
|
package.loaded["src.core.Sound"] = {
|
|
play = function() end,
|
|
playCry = function() end,
|
|
}
|
|
-- Real TextBoxes want a Font atlas. This flow only cares that a message
|
|
-- opened, what it says, and what its onDone does.
|
|
package.loaded["src.render.TextBox"] = {
|
|
new = function(_, text, done) return { textBox = true, text = text, done = done } 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.
|
|
package.loaded["src.ui.BagMenu"] = nil
|
|
package.loaded["src.ui.PartyMenu"] = nil
|
|
local BagMenu = require("src.ui.BagMenu")
|
|
local PartyMenu = require("src.ui.PartyMenu")
|
|
require("src.ui.Screens").invalidate()
|
|
|
|
local Fixtures = require("tests.modkit.fixtures")
|
|
local Bag = require("src.inventory.Bag")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
|
|
local Data = Fixtures.fresh()
|
|
-- The fixture item table has no candy of its own; ItemEffects keys the
|
|
-- level-up branch on the id, and BagMenu only reads name/keyItem off the def.
|
|
Data.items.RARE_CANDY = {
|
|
id = "RARE_CANDY", index = 90, name = "RARE CANDY", price = 4800,
|
|
tossable = true,
|
|
}
|
|
|
|
-- The mon: FIXMON_C has an empty `evolutions` and a learnset that stops at
|
|
-- level 1, so the 5 -> 6 candy prints its line and nothing else follows it.
|
|
-- That keeps the assertions on the bag rather than on the stat box, which
|
|
-- would need real graphics.
|
|
local function freshGame(candies)
|
|
local mon = Pokemon.new(Data, "FIXMON_C", 5)
|
|
local game = {
|
|
data = Data,
|
|
save = {
|
|
party = { mon },
|
|
player = { name = "RED", id = 1 },
|
|
inventory = {},
|
|
options = {},
|
|
flags = {},
|
|
money = 0,
|
|
},
|
|
}
|
|
game.stack = {
|
|
states = {},
|
|
push = function(self, s) table.insert(self.states, s) end,
|
|
pop = function(self) return table.remove(self.states) end,
|
|
top = function(self) return self.states[#self.states] end,
|
|
}
|
|
-- one button edge per update, the way Input reports a fixed step
|
|
game.input = { pressed = nil }
|
|
function game.input:wasPressed(b) return self.pressed == b end
|
|
-- FIX POTION first so the candy is never row 1: an index that silently
|
|
-- reset to the top would otherwise pass the cursor assertion by accident
|
|
Bag.add(game.save, "FIX_POTION", 1)
|
|
Bag.add(game.save, "RARE_CANDY", candies)
|
|
return game, mon
|
|
end
|
|
|
|
local function isPicker(s) return getmetatable(s) == PartyMenu end
|
|
local function isBox(s) return type(s) == "table" and s.textBox == true end
|
|
|
|
local function inStack(stack, pred)
|
|
for _, s in ipairs(stack.states) do
|
|
if pred(s) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function rowFor(list, id)
|
|
for i, r in ipairs(list.items) do
|
|
if r.value == id then return i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Open the bag, put the cursor on `id`, choose it, take USE off the
|
|
-- USE/TOSS box, then press A on the party picker. Returns the bag list.
|
|
local function useFromBag(game, battle, id)
|
|
local list = BagMenu.new(game, { battle = battle })
|
|
game.stack:push(list)
|
|
local row = rowFor(list, id)
|
|
if not row then return nil, "no " .. id .. " row in the bag" end
|
|
list.index = row
|
|
list.onChoose(list.items[row], list)
|
|
local sub = game.stack:top()
|
|
if not battle and sub and sub.items and sub.items[1]
|
|
and sub.items[1].onSelect then
|
|
game.stack:pop() -- the USE/TOSS Menu pops itself on select
|
|
sub.items[1].onSelect()
|
|
end
|
|
local picker = game.stack:top()
|
|
if not isPicker(picker) then return nil, "party picker never opened" end
|
|
game.input.pressed = "a"
|
|
picker:update(1 / 60)
|
|
game.input.pressed = nil
|
|
return list
|
|
end
|
|
|
|
-- The bug: three candies in the bag, use one in the field.
|
|
do
|
|
local game, mon = freshGame(3)
|
|
local list, why = useFromBag(game, nil, "RARE_CANDY")
|
|
if check(list ~= nil, "the bag opened and reached the picker: " .. tostring(why)) then
|
|
eq(mon.level, 6, "the candy leveled the mon 5 -> 6")
|
|
check(not inStack(game.stack, isPicker),
|
|
"the pickOnly picker popped itself before onSwitch")
|
|
check(inStack(game.stack, function(s) return s == list end),
|
|
"the bag list is STILL on the stack (.useItem_partyMenu re-enters "
|
|
.. "StartMenu_Item, it does not CloseStartMenu) (#796)")
|
|
|
|
local row = rowFor(list, "RARE_CANDY")
|
|
if check(row ~= nil, "the RARE CANDY row survived the use") then
|
|
eq(list.items[row].right, "x2", "and its count followed the inventory")
|
|
eq(list.index, row, "with the cursor left on it (wBagSavedMenuItem), "
|
|
.. "so the next candy is one A press away")
|
|
end
|
|
eq(game.save.inventory.RARE_CANDY, 2, "one candy was consumed")
|
|
|
|
local box = game.stack:top()
|
|
if check(isBox(box), "the grew-to-level line prints over the open bag") then
|
|
check(box.text:find("level 6", 1, true) ~= nil,
|
|
"and it names the new level: " .. tostring(box.text))
|
|
end
|
|
end
|
|
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
|
|
local game = freshGame(1)
|
|
local list = useFromBag(game, nil, "RARE_CANDY")
|
|
if check(list ~= nil, "the bag reached the picker with a single candy") then
|
|
check(inStack(game.stack, function(s) return s == list end),
|
|
"the last candy does not close the bag either (#796)")
|
|
check(rowFor(list, "RARE_CANDY") == nil, "its row was removed")
|
|
eq(game.save.inventory.RARE_CANDY, nil, "and the slot is empty")
|
|
check(list.index >= 1 and list.index <= #list.items,
|
|
"the cursor clamped to a valid row (index " .. tostring(list.index)
|
|
.. " of " .. #list.items .. ")")
|
|
end
|
|
end
|
|
|
|
-- Boundary the fix must not have moved: a candy is refused mid-battle, so the
|
|
-- field behavior above can never be mistaken for a battle regression.
|
|
-- item_effects.asm:800-803, ItemUseVitamin reads wIsInBattle and
|
|
-- `jp nz, ItemUseNotTime` before it ever falls into ItemUseMedicine, which is
|
|
-- why the port's battle-side branch is a guard rather than a live path. A
|
|
-- bare table stands in for the battle: nothing on this route reads it.
|
|
do
|
|
local game, mon = freshGame(3)
|
|
local list = useFromBag(game, { fakeBattle = true }, "RARE_CANDY")
|
|
if check(list ~= nil, "the battle bag reached the picker") then
|
|
eq(mon.level, 5, "a RARE CANDY mid-battle levels nothing (ItemUseVitamin "
|
|
.. "-> ItemUseNotTime)")
|
|
eq(game.save.inventory.RARE_CANDY, 3, "and is not consumed")
|
|
local box = game.stack:top()
|
|
if check(isBox(box), "the refusal prints") then
|
|
check(box.text:find("time to use", 1, true) ~= nil,
|
|
"with ItemUseNotTime's line: " .. tostring(box.text))
|
|
end
|
|
end
|
|
end
|
|
|
|
T.finish()
|