-- Parity test: using a RARE CANDY from the bag returns to the bag (#796). -- -- pokered files RARE_CANDY under UsableItems_PartyMenu (data/items/ -- use_party.asm), so after UseItem runs, start_sub_menus.asm's -- .useItem_partyMenu jumps back to StartMenu_Item with wBagSavedMenuItem -- still pointing at the candy -- the level text, PrintStatsBox, the -- level-up moves and any level evolution all happen first, then the item -- list is back with the cursor on the candy, which is what lets the -- original button-mash through a stack of them. The port closed the bag -- list before the level-up sequence and never reopened it, so every candy -- cost a full START -> ITEM trip. -- -- Self-contained; run via `luajit tests/parity_rare_candy_menu.lua`. package.path = "./?.lua;./?/init.lua;" .. package.path if not _G.love then _G.love = require("tests.love_stub") end local S = require("tests.harness").suite("parity rare candy menu") local check, eq = S.check, S.eq local Data = require("src.core.Data") Data:load() local Pokemon = require("src.pokemon.Pokemon") local Bag = require("src.inventory.Bag") -- Real TextBoxes want a Font atlas; the flow under test only cares which -- states land on the stack and what each onDone does. BagMenu binds -- TextBox at require time, so it is reloaded against the stub here and -- dropped again at the bottom. local realTextBox = package.loaded["src.render.TextBox"] local realBag = package.loaded["src.ui.BagMenu"] local realParty = package.loaded["src.ui.PartyMenu"] package.loaded["src.render.TextBox"] = { new = function(_, text, done) return { textBox = true, text = text, done = done } end, } 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() -- A stack that behaves like StateStack for the two things this flow reads: -- top() identity (ListMenu:close) and push/pop ordering. local function newStack() local stack = { states = {} } function stack:push(s) self.states[#self.states + 1] = s end function stack:pop() return table.remove(self.states) end function stack:top() return self.states[#self.states] end return stack end -- One button per call: PartyMenu:update reads game.input once per fixed step. local function newInput() local input = { pressed = nil } function input:wasPressed(b) return self.pressed == b end return input end -- CHARIZARD learns nothing at level 51 and has no evolution left, so a -- candy on it ends after the stat window -- no MoveLearnMenu, no -- EvolutionState clouding which state the stack returns to. local function freshGame(candies) local lead = Pokemon.new(Data, "CHARIZARD", 50) local game = { data = Data, stack = newStack(), input = newInput(), save = { party = { lead }, player = { name = "RED" }, inventory = {}, options = { battleStyle = "set", battleAnim = "on" }, pokedex = { seen = {}, owned = {} }, flags = {}, money = 0, }, } Bag.add(game.save, "RARE_CANDY", candies or 3) return game, lead end local function isPicker(s) return getmetatable(s) == PartyMenu end local function isBox(s) return type(s) == "table" and s.textBox == true end -- TextBox pops itself BEFORE firing onDone. local function dismiss(stack, box) if stack:top() == box then stack:pop() end if box.done then box.done() end 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 -- From an already-open bag list: choose the candy, take USE off the -- submenu, press A on the party picker. Returns the level-up text box. local function useCandy(game, list) local row = rowFor(list, "RARE_CANDY") if not row then return nil, "no RARE CANDY row in the bag" end list.index = row list.onChoose(list.items[row], list) -- out of battle the bag offers USE / TOSS first (start_sub_menus.asm) local sub = game.stack:top() if sub and sub.items and sub.items[1] and sub.items[1].onSelect then game.stack:pop() 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 game.stack:top() end -- Walk the rest of the level-up sequence: dismiss the level text, then A -- through the stat window (PrintStatsBox). local function finishLevelUp(game, box) dismiss(game.stack, box) local top = game.stack:top() if isBox(top) then return end -- a "learned MOVE" line, dismissed by caller if top and top.update then game.input.pressed = "a" top:update(1 / 60) game.input.pressed = nil end end -- ---- the report: one candy closed the whole menu ------------------------- do local game, lead = freshGame(3) local list = BagMenu.new(game, {}) game.stack:push(list) local row = rowFor(list, "RARE_CANDY") check(row ~= nil, "the candy is in the bag") local box = useCandy(game, list) check(isBox(box), "the level text opened") eq(lead.level, 51, "the candy leveled the mon") eq(game.save.inventory.RARE_CANDY, 2, "the candy was consumed") check(game.stack.states[1] == list, "the bag list is STILL on the stack under the level text (#796)") finishLevelUp(game, box) eq(game.stack:top(), list, "after the stat window the bag is back on top (StartMenu_Item)") eq(list.index, row, "the cursor is still on the RARE CANDY row") eq(list.items[row] and list.items[row].right, "x2", "the count refreshed in place") -- the point of the original's behavior: a second candy needs no menu trip local box2 = useCandy(game, list) check(isBox(box2), "a second candy fires from the still-open bag") eq(lead.level, 52, "and it levels the mon again") finishLevelUp(game, box2) eq(game.stack:top(), list, "still on the bag after the second candy") eq(game.save.inventory.RARE_CANDY, 1, "two candies spent") end -- ---- the last candy: the row empties but the bag still stays up ---------- do local game = freshGame(1) local list = BagMenu.new(game, {}) game.stack:push(list) local box = useCandy(game, list) check(isBox(box), "the last candy levels too") finishLevelUp(game, box) eq(game.stack:top(), list, "the bag stays open after the last candy") eq(#list.items, 0, "the emptied row left the list") eq(game.save.inventory.RARE_CANDY, nil, "no candies left in the inventory") end package.loaded["src.render.TextBox"] = realTextBox package.loaded["src.ui.BagMenu"] = realBag package.loaded["src.ui.PartyMenu"] = realParty require("src.ui.Screens").invalidate() S.finish()