Files
gen1recomp/src/inventory/Bag.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

141 lines
4.8 KiB
Lua

-- Gen 2 keeps FOUR pockets, not Gen 1's single bag (item_data_constants.asm):
-- Items 20, Balls 12, Key Items 25, TM/HM 57. A distinct item id occupies one
-- slot of ITS OWN pocket regardless of quantity, and a pocket fills
-- independently -- which is exactly why the cart can hold every TM, every key
-- item AND still pick up an HM. Modelling all four as one 20-slot list filled
-- the bag with TMs and key items by the Ice Path and refused HM07 WATERFALL.
-- Badges live in the inventory table but are not bag items. save.bagOrder
-- keeps acquisition order like wBagItems (SELECT can reorder it).
local Bag = {}
-- MAX_ITEMS / MAX_BALLS / MAX_KEY_ITEMS, and the TM/HM pocket holds one of
-- every TM plus the seven HMs (NUM_TMS + NUM_HMS). A `mods` bagSize override
-- replaces the ITEM pocket only, the way the Gen 1 single-bag config did.
local POCKET_CAPACITY = {
ITEM = 20,
BALL = 12,
KEY_ITEM = 25,
TM_HM = 64,
}
local DEFAULT_CAPACITY = 20
local function isBadge(id)
return id:find("BADGE", 1, true) ~= nil
end
-- Which pocket an id belongs to. Unknown ids (a stale cache, a mod that did
-- not declare a pocket) fall to ITEM, the Gen 1 behaviour.
local function pocketOf(id, data)
data = data or require("src.core.Data")
local def = data and data.items and data.items[id]
return (def and def.pocket) or "ITEM"
end
Bag.pocketOf = pocketOf
-- `data` is injectable for the save editor and headless mod tests. Normal
-- gameplay may omit it because the loader merges mods into the Data
-- singleton before any item can be added. A pocket argument gives that
-- pocket's cap; omitting it keeps the old single-number ITEM answer so
-- existing callers (and the mod bagSize override) are unchanged.
function Bag.capacity(data, pocket)
data = data or require("src.core.Data")
if pocket and pocket ~= "ITEM" then
return POCKET_CAPACITY[pocket] or DEFAULT_CAPACITY
end
local configured = data and data.constants and data.constants.bagSize
if type(configured) == "number" and configured >= 1 then
return math.floor(configured)
end
return POCKET_CAPACITY.ITEM
end
-- exported so item lists that share save.inventory (e.g. the PC deposit
-- menu) can exclude badges the same way the bag does
Bag.isBadge = isBadge
-- Occupied slots, of one pocket when named or of the whole inventory when not.
function Bag.slots(save, data, pocket)
local n = 0
for id in pairs(save.inventory) do
if not isBadge(id)
and (not pocket or pocketOf(id, data) == pocket) then
n = n + 1
end
end
return n
end
-- Acquisition-ordered id list (wBagItems). Rebuilt sorted once for
-- saves from before the order existed, then maintained incrementally.
function Bag.order(save)
local order = save.bagOrder
if not order then
order = {}
for id in pairs(save.inventory) do
if not isBadge(id) then table.insert(order, id) end
end
table.sort(order)
save.bagOrder = order
end
-- drop stale ids, append unknown ones (defensive against direct
-- inventory writes)
local seen = {}
for i = #order, 1, -1 do
local id = order[i]
if not save.inventory[id] or seen[id] then
table.remove(order, i)
else
seen[id] = true
end
end
for id in pairs(save.inventory) do
if not isBadge(id) and not seen[id] then table.insert(order, id) end
end
return order
end
-- Add qty of an item; returns false (and adds nothing) when a new slot
-- is needed and the bag is full, or when the stack would pass 99
-- (AddItemToInventory's per-slot quantity cap).
function Bag.add(save, id, qty, data)
local inv = save.inventory
-- Only the item's OWN pocket has to have room -- a full ITEM pocket does not
-- keep a KEY_ITEM or an HM out, which is the whole point of pockets.
local pocket = pocketOf(id, data)
if not inv[id] and not isBadge(id)
and Bag.slots(save, data, pocket) >= Bag.capacity(data, pocket) then
return false
end
if not isBadge(id) and (inv[id] or 0) + (qty or 1) > 99 then
return false
end
-- Insert into the order BEFORE the inventory write: Bag.order's defensive
-- append reads save.inventory, so writing first made it add the id and the
-- table.insert below add it again -- one pickup, two bag rows, until the
-- next order() pass deduped it (and a save taken in between kept both).
local isNew = not inv[id]
if isNew and not isBadge(id) then
table.insert(Bag.order(save), id)
end
inv[id] = (inv[id] or 0) + (qty or 1)
return true
end
-- Remove qty (default 1); clears the slot and its order entry at zero.
function Bag.remove(save, id, qty)
local inv = save.inventory
inv[id] = (inv[id] or 0) - (qty or 1)
if inv[id] <= 0 then
inv[id] = nil
local order = save.bagOrder
if order then
for i, oid in ipairs(order) do
if oid == id then table.remove(order, i) break end
end
end
end
end
return Bag