diff --git a/src/ui/gen2/MartMenu.lua b/src/ui/gen2/MartMenu.lua index acc63b6e..2fff4764 100644 --- a/src/ui/gen2/MartMenu.lua +++ b/src/ui/gen2/MartMenu.lua @@ -780,6 +780,11 @@ function MartMenu:offerToSell(itemId, count) end -- ScrollingMenu's .a_button copies the row's own quantity into -- wItemQuantity, so the selector's ceiling is how many you hold. + -- The unit price is the item's own ItemAttributes price: GetItemPrice is + -- what SelectQuantityToSell halves AND what the buy list's + -- GetMartItemPrice charges, so a TM sells for exactly half of what the + -- Goldenrod/Celadon TM shelves ask for it, and half of its hidden price + -- (usually ¥1,500) anywhere else (issue #1243). self.qtyItem = { id = itemId, name = def.name or itemId, price = def.price or 0 } self.qty = 1 diff --git a/src/ui/gen2/PackMenu.lua b/src/ui/gen2/PackMenu.lua index 7c84f3c4..e4c8d3a4 100644 --- a/src/ui/gen2/PackMenu.lua +++ b/src/ui/gen2/PackMenu.lua @@ -354,20 +354,23 @@ function PackMenu:useSelected() end return end - -- engine/items/tmhm.asm:73 - local def = self.items and self.items[row.id] - if def and def.teaches then - self:openTeachParty(row) - return - end - -- UseItem's jumptable runs off ITEMATTR's field-menu nibble, and the first - -- four entries are all .Oak -- an X ATTACK or a POKé DOLL used from the - -- field PACK prints OakThisIsntTheTimeText and goes nowhere. Only the - -- FIELD pack owns that refusal: the battle pack returned above, and the - -- catch tutorial's DUDE pack carries a stub world with no useFieldItem at - -- all -- its POKE BALL is field-NOUSE and must still reach the throw. + -- UseItem's FIELD-pack tail (engine/items/tmhm.asm:73): a TM/HM row opens + -- the party to teach, and a field-NOUSE item -- an X ATTACK or a POKé DOLL + -- used from the field PACK -- prints OakThisIsntTheTimeText and goes + -- nowhere (UseItem's jumptable's first four entries are all .Oak). Both + -- checks live behind `world.useFieldItem` on purpose: DepositSellPack (the + -- mart's SELL, the item PC's DEPOSIT) is a chooser whose jumptable is four + -- ScrollingMenus and never reaches tmhm.asm, so a TM picked there hands + -- its row to onChoose like every other item instead of opening the teach + -- party. The battle pack returned above, and the catch tutorial's DUDE + -- pack carries a stub world with no useFieldItem at all -- its POKE BALL + -- is field-NOUSE and must still reach the throw. if world and world.useFieldItem then local def = self.items and self.items[row.id] + if def and def.teaches then + self:openTeachParty(row) + return + end if def and def.fieldMenu == "ITEMMENU_NOUSE" then self.message = OAK_THIS_ISNT_THE_TIME return diff --git a/tests/gen2_field_items_test.lua b/tests/gen2_field_items_test.lua index 912e61ad..f7589f06 100644 --- a/tests/gen2_field_items_test.lua +++ b/tests/gen2_field_items_test.lua @@ -549,6 +549,27 @@ do dudePack:useSelected() eq(thrown, "POKE_BALL", "the tutorial pack still throws") eq(dudePack.message, nil, "with no Oak line in the way") + + -- DepositSellPack passes the same empty world (the mart's SELL and the + -- item PC's DEPOSIT both do): a TM row must hand its id back rather than + -- open the teach party, which is issue #1243's "Teach which PKMN?" in + -- the middle of a sale. + local sellGame = { input = newInput(), save = { + player = { name = "GOLD" }, + inventory = { TM01 = 1 }, + options = {}, + }, data = DATA, stack = newStack() } + local sold + local sellPack = PackMenu.new(sellGame, { + save = sellGame.save, items = DATA.items, world = {}, + onChoose = function(id) sold = id end, + }) + sellPack.pocketIndex = 4 -- TM_HM + sellPack:rebuild() + sellPack:useSelected() + eq(sold, "TM01", "a chooser pack hands a TM to its caller") + eq(sellPack.message, nil, "with no teach refusal in the way") + eq(#sellGame.stack._items, 0, "and nothing pushed over the pack") end -- ------------------------------------------------ say(): the teardown rule diff --git a/tests/gen2_menus_test.lua b/tests/gen2_menus_test.lua index 00054183..a6bc6507 100644 --- a/tests/gen2_menus_test.lua +++ b/tests/gen2_menus_test.lua @@ -1481,6 +1481,11 @@ local martItems = { price = 100, canToss = true }, NUGGET = { id = "NUGGET", name = "NUGGET", pocket = "ITEM", index = 36, price = 10000, canToss = true }, + -- A TM carries the move it teaches; the SELL pack must hand it to the + -- mart rather than opening the teach party (issue #1243). + TM_HEADBUTT = { id = "TM_HEADBUTT", name = "TM02", pocket = "TM_HM", + index = 234, price = 2000, canToss = true, teaches = "HEADBUTT", + tmNumber = 2 }, -- Every KEY ITEM carries CANT_TOSS, which is the flag SellMenu's -- _CheckTossableItem actually refuses on. BICYCLE = { id = "BICYCLE", name = "BICYCLE", pocket = "KEY_ITEM", index = 7, @@ -1732,6 +1737,35 @@ sellInput:press("a") sell:update(0) -- YES check("the wallet clamps at MAX_MONEY", sellSave.player.money, 999999) check("and the nugget is gone", sellSave.inventory.NUGGET, nil) +-- A TM is not a key item: DepositSellPack's jumptable is four ScrollingMenus +-- and has no teach arm, so picking a TM from the SELL pack must ask "How +-- many?" and price it at half of its ItemAttributes price -- never open the +-- teach party (issue #1243). +local tmSave = Save.newGame() +tmSave.inventory = { TM_HEADBUTT = 1 } +tmSave.player.money = 0 +local tm, tmInput = newMart(tmSave) +tmInput:press("down") tm:update(0) +tmInput:press("a") tm:update(0) -- SELL +check("SELL builds the pack", tm.pack ~= nil, true) +-- The PACK opens on the ITEM pocket; cross to TM/HM the way the player would. +tm.pack.pocketIndex = 4 +tm.pack:rebuild() +check("the TM is the TM pocket's row", tm.pack.rows[1].id, "TM_HEADBUTT") +tmInput:press("a") tm:update(0) +check("picking a TM asks how many", tm.phase, "sellQuantity") +check("with no teach screen opened", #tm.game.stack._items, 0) +check("and no pack refusal printed", tm.pack.message, nil) +check("at the TM's own price", tm.qtyItem.price, 2000) +check("and the ceiling is what you hold", tm.qtyMax, 1) +tmInput:press("a") tm:update(0) +check("the offer prices half of the TM", tm.confirm.pages[1][2], + "\xc2\xa51000.") +tmInput:press("a") tm:update(0) -- page 2 +tmInput:press("a") tm:update(0) -- YES +check("the TM leaves the bag", tmSave.inventory.TM_HEADBUTT, nil) +check("and the money arrives", tmSave.player.money, 1000) + -- ------------------------------------------------- PlayTransactionSound -- -- engine/items/mart.asm rings SFX_TRANSACTION in exactly two places, both of diff --git a/tests/gen2_pc_screens_test.lua b/tests/gen2_pc_screens_test.lua index 44bec0b3..1c6f30dc 100644 --- a/tests/gen2_pc_screens_test.lua +++ b/tests/gen2_pc_screens_test.lua @@ -94,6 +94,8 @@ local ITEMS = { canToss = false }, HM_CUT = { id = "HM_CUT", name = "HM01", pocket = "TM_HM", index = 0xf3, canToss = false }, + TM_HEADBUTT = { id = "TM_HEADBUTT", name = "TM02", pocket = "TM_HM", + index = 0xea, canToss = true, teaches = "HEADBUTT" }, } local function newGame(save, items) @@ -291,6 +293,23 @@ do eq(pc.phase, "menu", "B drops back to the item PC menu") end +-- The same DepositSellPack chooser holds a TM: it must reach the quantity +-- selector like everything tossable, never the teach party (issue #1243). +do + local save = newSave(1) + local game, input = newGame(save) + Bag.add(save, "TM_HEADBUTT", 1, game.data) + local pc = ItemPcMenu.new(game, { save = save, items = ITEMS }) + press(pc, input, "down", "a") -- DEPOSIT ITEM + press(pc, input, "right", "right", "right") -- ITEM -> BALL -> KEY_ITEM -> TM_HM + press(pc, input, "a") -- choose the TM + check(pc.qtyState ~= nil, "a TM asks how many to deposit") + eq(#game.stack._items, 0, "with no teach screen pushed over the pack") + press(pc, input, "a") -- deposit x1 + eq(save.pcItems.TM_HEADBUTT, 1, "the TM lands in the PC") + eq(save.inventory.TM_HEADBUTT, nil, "and leaves the bag") +end + -- .TryDepositItem's .no_toss: a KEY ITEM stays in the bag, silently. do local save = newSave(1)