-- Gen 2 storage system: 14 boxes of 20, the party<->box moves the PC does, and -- the default BOX1..BOX14 names. -- -- The save already carries `boxes`, `boxNames` and `currentBox` -- (src/core/gen2/Save.lua); this is the logic that operates on them, kept out -- of the UI so a deposit is testable without a screen. -- -- Rules taken from engine/pokemon/bills_pc.asm: -- * the PC cannot be opened with an empty party (.CheckCanUsePC) -- * DEPOSIT refuses to send the last healthy party mon away, because a party -- of nothing whites you out on the next step -- * WITHDRAW refuses once the party is full -- * a box holds MONS_PER_BOX and no more -- * DEPOSIT refuses a mon holding MAIL, because sPartyMail has six slots and -- a boxed mon has none of them (src/core/gen2/Mail.lua) local Mail = require("src.core.gen2.Mail") local Save = require("src.core.gen2.Save") local Boxes = {} Boxes.NUM_BOXES = Save.NUM_BOXES Boxes.MONS_PER_BOX = Save.MONS_PER_BOX Boxes.PARTY_SIZE = Save.PARTY_SIZE -- SetDefaultBoxNames (engine/menus/intro_menu.asm): "BOX" then 1..14. function Boxes.defaultName(index) return "BOX" .. tostring(index) end function Boxes.name(save, index) local names = save and save.boxNames local given = names and names[index] if type(given) == "string" and given ~= "" then return given end return Boxes.defaultName(index) end function Boxes.rename(save, index, name) if not save or not index then return false end if index < 1 or index > Boxes.NUM_BOXES then return false end save.boxNames = save.boxNames or {} save.boxNames[index] = name return true end -- The box's mon list, created on demand so a fresh save carries 14 empty -- tables only once one is actually used. function Boxes.box(save, index) if not save then return {} end index = index or save.currentBox or 1 if index < 1 or index > Boxes.NUM_BOXES then return {} end save.boxes = save.boxes or {} save.boxes[index] = save.boxes[index] or {} return save.boxes[index] end function Boxes.count(save, index) return #Boxes.box(save, index) end function Boxes.isFull(save, index) return Boxes.count(save, index) >= Boxes.MONS_PER_BOX end function Boxes.setCurrent(save, index) if not save or index < 1 or index > Boxes.NUM_BOXES then return false end save.currentBox = index return true end -- How many party members could still fight. DEPOSIT checks this, not the raw -- party count: a party of one fainted mon plus one healthy one may not send -- the healthy one to a box. function Boxes.healthyCount(party) local n = 0 for _, mon in ipairs(party or {}) do if (mon.hp or 0) > 0 then n = n + 1 end end return n end -- Returns true, or false plus a reason string the caller shows in a text box. function Boxes.canDeposit(save, partyIndex, boxIndex) if not save then return false, "No save." end local mon = save.party and save.party[partyIndex] if not mon then return false, "There is no POKéMON there." end if Boxes.isFull(save, boxIndex) then return false, "The BOX is full." end if (mon.hp or 0) > 0 and Boxes.healthyCount(save.party) <= 1 then return false, "You can't deposit\nthe last POKéMON!" end -- BillsPC_CheckMon's .HasMail arm (engine/pokemon/bills_pc.asm), which reads -- the wBillsPC_MonHasMail byte PCMonInfo set while drawing the row. It is -- checked AFTER the last-healthy rule and prints PCString_RemoveMail, which -- is one short line rather than a two-line refusal: a boxed mon's letter has -- nowhere to live, because sPartyMail is six structs keyed by party slot. if Mail.monHoldsMail(mon) then return false, "Remove MAIL." end return true end function Boxes.deposit(save, partyIndex, boxIndex) local ok, reason = Boxes.canDeposit(save, partyIndex, boxIndex) if not ok then return false, reason end local mon = table.remove(save.party, partyIndex) -- RemoveMonFromPartyOrBox's "Mail time!" tail: sPartyMail is keyed by SLOT, -- so every letter after the departing mon moves up one. The mon leaving -- here never has mail of its own (canDeposit just refused that), but the -- ones behind it may. Mail.removeSlot(save, partyIndex) local box = Boxes.box(save, boxIndex) box[#box + 1] = mon return true, mon end function Boxes.canWithdraw(save, boxIndex, slot) if not save then return false, "No save." end local box = Boxes.box(save, boxIndex) if not box[slot] then return false, "There is no POKéMON there." end if #(save.party or {}) >= Boxes.PARTY_SIZE then return false, "You can't take\nany more POKéMON." end return true end function Boxes.withdraw(save, boxIndex, slot) local ok, reason = Boxes.canWithdraw(save, boxIndex, slot) if not ok then return false, reason end local mon = table.remove(Boxes.box(save, boxIndex), slot) save.party = save.party or {} save.party[#save.party + 1] = mon return true, mon end -- RELEASE from a box. The cart lets you release anything in storage; the -- party's last-healthy rule does not apply because a boxed mon is never in it. function Boxes.release(save, boxIndex, slot) local box = Boxes.box(save, boxIndex) if not box[slot] then return false, "There is no POKéMON there." end return true, table.remove(box, slot) end -- Move a boxed mon to another box (MOVE PKMN W/O MAIL's box-to-box case). function Boxes.move(save, fromBox, slot, toBox) if fromBox == toBox then return false, "It's already there." end local source = Boxes.box(save, fromBox) if not source[slot] then return false, "There is no POKéMON there." end if Boxes.isFull(save, toBox) then return false, "The BOX is full." end local mon = table.remove(source, slot) local target = Boxes.box(save, toBox) target[#target + 1] = mon return true, mon end -- .CheckCanUsePC: "You'll need a POKéMON to call with." function Boxes.canUsePc(save) if not (save and save.party and #save.party > 0) then return false, "You'll need a\nPOKéMON to call\nwith." end return true end return Boxes