mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #558 from erereck/fix/mod-configurable-bag-capacity
This commit is contained in:
@@ -12,7 +12,7 @@ this port.
|
||||
|
||||
| # | Mechanic | Value | Why the Game Boy had this limit | Where it lives here |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Bag capacity | 20 item slots | `wNumBagItems` save block was a fixed 20-entry id/quantity array in SRAM | `src/inventory/Bag.lua:8` (`Bag.CAPACITY = 20`) |
|
||||
| 1 | Bag capacity | 20 item slots by default | `wNumBagItems` save block was a fixed 20-entry id/quantity array in SRAM | `Data.constants.bagSize`, read by `src/inventory/Bag.lua` (`Bag.capacity`) |
|
||||
| 2 | Party size | 6 Pokémon | `wPartyMon1..6` were 6 fixed save-RAM slots | `src/pokemon/Party.lua:5` (`Party.MAX = 6`) |
|
||||
| 3 | PC storage | 12 boxes × 20 Pokémon | `wBoxDataStart` / Bill's PC allocated a fixed 12×20 SRAM block | `src/pokemon/Boxes.lua:7-8` |
|
||||
| 4 | Moves per Pokémon | 4 | Fixed 4-move-slot field in the party/box Pokémon struct | `src/pokemon/Pokemon.lua:20`, enforced again in `src/battle/BattleState.lua:1941` |
|
||||
@@ -38,6 +38,10 @@ this port.
|
||||
|
||||
## Notes
|
||||
|
||||
- Mods may patch `constants.bagSize` through the public content registry. The
|
||||
native `save.lua` format keeps every existing item when the configured
|
||||
limit changes; exporting to a cartridge `.sav` still writes only the first
|
||||
20 bag slots because the original SRAM layout has no room for more.
|
||||
- PC Box **overflow handling** was deliberately changed even though the
|
||||
20×12 box *shape* was kept faithful: instead of Gen 1's "full box discards
|
||||
or blocks the deposit," this port spills into the next box with room.
|
||||
|
||||
@@ -380,7 +380,8 @@ semantics - so the two windows read as one app. Six tabs:
|
||||
party dock, so deposit and withdraw live in one place. Empty slots are
|
||||
clickable and create a mon there.
|
||||
- **Items**: money, a searchable item picker (replacing the arrows that
|
||||
cycled one id at a time through ~250 items), the 20-slot bag, PC storage
|
||||
cycled one id at a time through ~250 items), the configurable bag (20 slots
|
||||
by default), PC storage
|
||||
with no slot cap, and the eight badges as toggle chips.
|
||||
- **Events**: flags, defeated trainers, taken items and per-map object
|
||||
toggles, with a real filter field and a two-column paged grid.
|
||||
|
||||
@@ -156,7 +156,7 @@ return function(mod)
|
||||
caughtAreas()[areaKey(self.game, self)] = "DUPES_LOST"
|
||||
mod.save:set("caught_areas", caughtAreas())
|
||||
end
|
||||
Bag.add(self.game.save, ball, 1)
|
||||
Bag.add(self.game.save, ball, 1, self.game.data)
|
||||
self:say(reason == "area" and "This area already\nhas a captured POKéMON!"
|
||||
or "You already have\nthis POKéMON family!")
|
||||
return
|
||||
|
||||
+5
-5
@@ -14,12 +14,12 @@ local MODULES = {
|
||||
-- Optional for compatibility with developer and stale caches.
|
||||
local OPTIONAL = { "audio", "palettes", "icons" }
|
||||
|
||||
-- The rules the engine still carries as literals. The constants registry
|
||||
-- deep-merges over these, so a value has to exist before a mod can patch
|
||||
-- it; each one is the number the engine hard-codes today, so seeding them
|
||||
-- changes nothing on a mod-free boot.
|
||||
-- Vanilla defaults for rules exposed through the constants registry. A
|
||||
-- value has to exist before a mod can patch it; each one matches the
|
||||
-- engine's no-mod behavior, so seeding them changes nothing on a vanilla
|
||||
-- boot.
|
||||
local CONSTANT_DEFAULTS = {
|
||||
bagSize = 20, -- BAG_ITEM_CAPACITY (src/inventory/Bag.lua)
|
||||
bagSize = 20, -- BAG_ITEM_CAPACITY (Bag.capacity fallback)
|
||||
partyMax = 6, -- PARTY_LENGTH (src/pokemon/Party.lua)
|
||||
boxCount = 12, boxSize = 20, -- Bill's PC (src/pokemon/Boxes.lua)
|
||||
moveMax = 4,
|
||||
|
||||
@@ -1058,7 +1058,7 @@ local function reclaim(save, data, report)
|
||||
if type(entry) == "table" and known(data.items, entry.id) then
|
||||
table.remove(orphaned.items, i)
|
||||
if entry.from == "pcItems" or type(save.inventory) ~= "table"
|
||||
or not Bag.add(save, entry.id, entry.count or 1) then
|
||||
or not Bag.add(save, entry.id, entry.count or 1, data) then
|
||||
save.pcItems = save.pcItems or {}
|
||||
save.pcItems[entry.id] = (save.pcItems[entry.id] or 0) + (entry.count or 1)
|
||||
end
|
||||
|
||||
+1
-1
@@ -214,7 +214,7 @@ function VERBS.give(self, rest)
|
||||
end
|
||||
elseif game.data.items and game.data.items[id] then
|
||||
local n = tonumber(count) or 1
|
||||
if require("src.inventory.Bag").add(save, id, n) then
|
||||
if require("src.inventory.Bag").add(save, id, n, game.data) then
|
||||
self:print(("%s x%d added"):format(id, n))
|
||||
else
|
||||
self:print("bag full")
|
||||
|
||||
+23
-7
@@ -1,11 +1,26 @@
|
||||
-- The 20-slot bag (BAG_ITEM_CAPACITY, constants/menu_constants.asm):
|
||||
-- a distinct item id occupies one slot regardless of quantity; badges
|
||||
-- live in the inventory table but are not bag items. save.bagOrder
|
||||
-- keeps acquisition order like wBagItems (SELECT can reorder it).
|
||||
-- The bag defaults to 20 slots (BAG_ITEM_CAPACITY,
|
||||
-- constants/menu_constants.asm), but mods may replace that limit through
|
||||
-- Data.constants.bagSize. A distinct item id occupies one slot regardless
|
||||
-- of quantity; badges live in the inventory table but are not bag items.
|
||||
-- save.bagOrder keeps acquisition order like wBagItems (SELECT can reorder
|
||||
-- it).
|
||||
|
||||
local Bag = {}
|
||||
|
||||
Bag.CAPACITY = 20
|
||||
local DEFAULT_CAPACITY = 20
|
||||
|
||||
-- `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. The fallback keeps old/stale
|
||||
-- generated caches and isolated callers at the vanilla limit.
|
||||
function Bag.capacity(data)
|
||||
data = data or require("src.core.Data")
|
||||
local configured = data and data.constants and data.constants.bagSize
|
||||
if type(configured) == "number" and configured >= 1 then
|
||||
return math.floor(configured)
|
||||
end
|
||||
return DEFAULT_CAPACITY
|
||||
end
|
||||
|
||||
local function isBadge(id)
|
||||
return id:find("BADGE", 1, true) ~= nil
|
||||
@@ -55,9 +70,10 @@ 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)
|
||||
function Bag.add(save, id, qty, data)
|
||||
local inv = save.inventory
|
||||
if not inv[id] and not isBadge(id) and Bag.slots(save) >= Bag.CAPACITY then
|
||||
if not inv[id] and not isBadge(id)
|
||||
and Bag.slots(save) >= Bag.capacity(data) then
|
||||
return false
|
||||
end
|
||||
if not isBadge(id) and (inv[id] or 0) + (qty or 1) > 99 then
|
||||
|
||||
@@ -210,11 +210,12 @@ end
|
||||
-- text (label or literal; {RAM:wStringBuffer} becomes the item name);
|
||||
-- pass false when the script shows its own received-text row.
|
||||
function Commands.give_item(ctx, itemId, count, gotText)
|
||||
-- the 20-slot bag can refuse (BAG_ITEM_CAPACITY): say so and halt
|
||||
-- the bag can refuse at its configured capacity (20 in vanilla): halt
|
||||
-- the script, so later set_flag rows don't burn the gift -- make
|
||||
-- room and talk again, like the original (pokered's `jr nc, .bag_full`
|
||||
-- skips the received text entirely when AddItemToInventory refuses)
|
||||
if not require("src.inventory.Bag").add(ctx.save, itemId, count or 1) then
|
||||
if not require("src.inventory.Bag").add(
|
||||
ctx.save, itemId, count or 1, ctx.game.data) then
|
||||
Commands.show_text(ctx, ctx.game.data.text
|
||||
and ctx.game.data.text._BagFullText or Strings("You can't carry\nany more items!"))
|
||||
return math.huge
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ local function withdraw(game)
|
||||
onChoose = function(item, list)
|
||||
askQuantity(game, list, pc[item.value] or 1, item.value, function(qty)
|
||||
local Bag = require("src.inventory.Bag")
|
||||
if not Bag.add(game.save, item.value, qty) then
|
||||
if not Bag.add(game.save, item.value, qty, game.data) then
|
||||
list.footer = Strings("You can't carry\nany more items.")
|
||||
return
|
||||
end
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ local function buy(game, stock)
|
||||
list.footer = notEnough
|
||||
return
|
||||
end
|
||||
if not Bag.add(game.save, item.value, qty) then
|
||||
if not Bag.add(game.save, item.value, qty, game.data) then
|
||||
list.footer = txt(game, "_PokemartItemBagFullText",
|
||||
Strings("You can't carry\nany more items."))
|
||||
return
|
||||
|
||||
@@ -1803,7 +1803,7 @@ function OverworldState:tryHiddenObject(fx, fy)
|
||||
if h.x == fx and h.y == fy then
|
||||
save.hiddenTaken = save.hiddenTaken or {}
|
||||
if save.hiddenTaken[key] then return false end
|
||||
if not require("src.inventory.Bag").add(save, h.item, 1) then
|
||||
if not require("src.inventory.Bag").add(save, h.item, 1, Game.data) then
|
||||
Game.stack:push(TextBox.new(Game, Strings("You can't carry\nany more items!")))
|
||||
return true
|
||||
end
|
||||
@@ -2423,7 +2423,7 @@ function OverworldState:talkTo(npc)
|
||||
-- (e.g. Blue's House wall Town Map / walking Daisy, #11). Lua treats
|
||||
-- the string "0" as truthy, so screen it out and fall through to text.
|
||||
if d.item and d.item ~= "0" and d.item ~= 0 then
|
||||
if not require("src.inventory.Bag").add(Game.save, d.item, 1) then
|
||||
if not require("src.inventory.Bag").add(Game.save, d.item, 1, Game.data) then
|
||||
Game.stack:push(TextBox.new(Game, Strings("You can't carry\nany more items!")))
|
||||
return
|
||||
end
|
||||
|
||||
@@ -2414,14 +2414,15 @@ local function sellItem(id)
|
||||
return sold
|
||||
end
|
||||
|
||||
-- Free up bag slots so `needed` NEW item kinds fit (Bag.CAPACITY is 20
|
||||
-- slots; a stack of an item already held costs nothing). This is why
|
||||
-- Free up bag slots so `needed` NEW item kinds fit (20 slots without a
|
||||
-- capacity mod; a stack of an item already held costs nothing). This is why
|
||||
-- every restock had been reporting "HYPER_POTION x0": the buy list
|
||||
-- opened, the quantity was set, the engine said "no room" -- and the run
|
||||
-- walked into the Mansion with FULL_HEALs but not one HP restore.
|
||||
local function freeBagSlots(needed, where)
|
||||
local used = #(G.save.bagOrder or {})
|
||||
local free = 20 - used
|
||||
local capacity = require("src.inventory.Bag").capacity(G.data)
|
||||
local free = capacity - used
|
||||
for _, id in ipairs(SELLABLE_JUNK) do
|
||||
if free >= needed then break end
|
||||
if ((G.save.inventory or {})[id] or 0) > 0 then
|
||||
@@ -2632,7 +2633,8 @@ function ops.shop(s, where)
|
||||
end
|
||||
end
|
||||
local used = #(G.save.bagOrder or {})
|
||||
if newKinds > 0 and 20 - used < newKinds then
|
||||
local capacity = require("src.inventory.Bag").capacity(G.data)
|
||||
if newKinds > 0 and capacity - used < newKinds then
|
||||
freeBagSlots(newKinds, where)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
-- T4: constants.bagSize controls the bag through the public mod API while
|
||||
-- vanilla and existing-save behavior remain unchanged.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Bag = require("src.inventory.Bag")
|
||||
|
||||
local CAPACITY_MOD = {
|
||||
["mods/fix_small_bag/manifest.json"] = [[{
|
||||
"id": "fix_small_bag",
|
||||
"name": "Fixture Small Bag",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"api": 2
|
||||
}]],
|
||||
["mods/fix_small_bag/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.content.constants:patch("bagSize", 2)
|
||||
]],
|
||||
}
|
||||
|
||||
-- No-mod parity: the new lookup keeps the cartridge's 20-slot limit.
|
||||
do
|
||||
local data = T.fixtures.fresh()
|
||||
local run = T.sdk.loadNone({ data = data })
|
||||
T.eq(#run.errors, 0, "the no-mod baseline loads cleanly")
|
||||
T.eq(Bag.capacity(data), 20, "the vanilla bag still has 20 slots")
|
||||
T.eq(Bag.capacity({}), 20, "a stale dataset without bagSize falls back to 20")
|
||||
run.release()
|
||||
end
|
||||
|
||||
-- The public constants registry changes both the reported and enforced cap.
|
||||
do
|
||||
local data = T.fixtures.fresh()
|
||||
local run = T.sdk.loadMods({ "mods/fix_small_bag" },
|
||||
{ data = data, fs = T.sdk.memfs(CAPACITY_MOD) })
|
||||
T.eq(#run.errors, 0, "the capacity mod loads cleanly")
|
||||
T.eq(Bag.capacity(data), 2, "Bag.capacity reads merged constants.bagSize")
|
||||
|
||||
local save = { inventory = {} }
|
||||
T.check(Bag.add(save, "FIX_POTION", 1, data), "the first item fits")
|
||||
T.check(Bag.add(save, "FIX_BALL", 1, data), "the second item fits")
|
||||
T.check(not Bag.add(save, "FIX_TM", 1, data),
|
||||
"a new item is refused at the modded limit")
|
||||
T.eq(Bag.slots(save), 2, "a refused add does not change the bag")
|
||||
run.release()
|
||||
end
|
||||
|
||||
-- Saves are dictionaries, not fixed arrays: lowering the active cap never
|
||||
-- truncates an older or modded save. Existing stacks remain usable while a
|
||||
-- new item waits until the player makes enough room.
|
||||
do
|
||||
local data = T.fixtures.fresh()
|
||||
data.constants.bagSize = 1
|
||||
local save = {
|
||||
inventory = { FIX_POTION = 1, FIX_BALL = 1 },
|
||||
bagOrder = { "FIX_POTION", "FIX_BALL" },
|
||||
}
|
||||
T.eq(Bag.slots(save), 2, "an over-cap save keeps all existing slots")
|
||||
T.check(Bag.add(save, "FIX_POTION", 1, data),
|
||||
"an over-cap save can still add to an existing stack")
|
||||
T.eq(save.inventory.FIX_POTION, 2, "the existing stack is updated")
|
||||
T.check(not Bag.add(save, "FIX_TM", 1, data),
|
||||
"an over-cap save cannot add another item kind")
|
||||
T.eq(Bag.slots(save), 2, "the compatibility path never drops items")
|
||||
end
|
||||
|
||||
T.finish("bag_capacity")
|
||||
@@ -181,12 +181,13 @@ end
|
||||
do
|
||||
-- the bag has a hard slot cap; the picker must refuse past it
|
||||
local S = newState()
|
||||
local capacity = Bag.capacity(S.data)
|
||||
local added = 0
|
||||
for _, id in ipairs(S.cat.items) do
|
||||
if not Ops.isBadgeId(id) and Ops.addToBag(S, id) then added = added + 1 end
|
||||
if added >= Bag.CAPACITY then break end
|
||||
if added >= capacity then break end
|
||||
end
|
||||
eq(Bag.slots(S.save), Bag.CAPACITY, "the bag filled to its cap")
|
||||
eq(Bag.slots(S.save), capacity, "the bag filled to its cap")
|
||||
S.dirty = false
|
||||
local spare
|
||||
for _, id in ipairs(S.cat.items) do
|
||||
|
||||
@@ -427,7 +427,7 @@ local function tabCount(id)
|
||||
return tostring(n)
|
||||
elseif id == "items" then
|
||||
local Bag = require("src.inventory.Bag")
|
||||
return ("%d/%d"):format(Bag.slots(S.save), Bag.CAPACITY)
|
||||
return ("%d/%d"):format(Bag.slots(S.save), Bag.capacity(S.data))
|
||||
elseif id == "events" then
|
||||
local n = 0
|
||||
for _ in pairs(S.save.flags or {}) do n = n + 1 end
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
--
|
||||
-- Clamps mirror the running game, not the UI: level 1-100, DV 0-15, party 6
|
||||
-- (src/pokemon/Party), box 20 x 12 (src/pokemon/Boxes), money 0-999999,
|
||||
-- item stack 99 and 20 bag slots (src/inventory/Bag).
|
||||
-- item stack 99 and the configured bag capacity (20 by default;
|
||||
-- src/inventory/Bag).
|
||||
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local PartyMod = require("src.pokemon.Party")
|
||||
@@ -338,11 +339,13 @@ end
|
||||
|
||||
function Ops.addToBag(S, id)
|
||||
if not id then return Ops.say(S, "Pick an item first") end
|
||||
if Bag.add(S.save, id, 1) then
|
||||
local capacity = Bag.capacity(S.data)
|
||||
if Bag.add(S.save, id, 1, S.data) then
|
||||
return Ops.mark(S, ("Added %s to the bag (%d/%d slots)")
|
||||
:format(id, Bag.slots(S.save), Bag.CAPACITY))
|
||||
:format(id, Bag.slots(S.save), capacity))
|
||||
end
|
||||
return Ops.say(S, ("Bag is full (%d/%d slots)"):format(Bag.slots(S.save), Bag.CAPACITY))
|
||||
return Ops.say(S, ("Bag is full (%d/%d slots)")
|
||||
:format(Bag.slots(S.save), capacity))
|
||||
end
|
||||
|
||||
function Ops.bagAdjust(S, id, delta)
|
||||
@@ -352,7 +355,7 @@ function Ops.bagAdjust(S, id, delta)
|
||||
if have >= Ops.STACK_MAX then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(id, Ops.STACK_MAX))
|
||||
end
|
||||
Bag.add(S.save, id, delta)
|
||||
Bag.add(S.save, id, delta, S.data)
|
||||
else
|
||||
Bag.remove(S.save, id, -delta)
|
||||
if not S.save.inventory[id] then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Items panel: money, the shared item picker, badges, the 20-slot bag
|
||||
-- Items panel: money, the shared item picker, badges, the configurable bag
|
||||
-- (Bag.add/remove, ordered by Bag.order) and PC item storage (a plain
|
||||
-- S.save.pcItems dict with no slot cap).
|
||||
--
|
||||
@@ -178,12 +178,13 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
|
||||
-- --------------------------------------------------------------- bag
|
||||
local order = Bag.order(S.save)
|
||||
local capacity = Bag.capacity(S.data)
|
||||
Kit.card(bagX, y, listW, h)
|
||||
Kit.caption(bagX + pad, y + pad, "BAG")
|
||||
Kit.textRight("mono", ("%d/%d slots"):format(Bag.slots(S.save), Bag.CAPACITY),
|
||||
Kit.textRight("mono", ("%d/%d slots"):format(Bag.slots(S.save), capacity),
|
||||
bagX + listW - pad, y + pad, PAL.caption)
|
||||
local barY = y + pad + Kit.textHeight("caption") + 8 * s
|
||||
local slotFrac = Bag.slots(S.save) / Bag.CAPACITY
|
||||
local slotFrac = Bag.slots(S.save) / capacity
|
||||
Kit.meter(bagX + pad, barY, listW - 2 * pad, 5 * s, slotFrac * 100,
|
||||
slotFrac >= 1 and PAL.yellow or PAL.blue)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user