Files
gen1recomp/tests/game3_storage_test.lua
T
1jamie c93f035359 fix(game3): hidden items, itemfinder, battle hud caught marker, moveset learnsets, and easy chat
- Hidden Items & Itemfinder (fixes #2314):
  * Extract BG_EVENT_HIDDEN_ITEM (kind 7) from GBA ROM with proper flag calculation (0x3E8 + id), underfoot flags, and quantities.
  * Implement overworld Itemfinder scan logic (7-tile radius, directional response, underfoot detection, SFX 65, and OEM text).
  * Wire up Bag Menu Itemfinder dispatch to overworld field runtime.

- Opponent Battle HUD Caught Marker (fixes #2313):
  * Add authentic Pokédex caught Poké Ball icon (B_INTERFACE_GFX_BALL_CAUGHT / tile 70) to opponent healthbox.
  * Match pokefirered parity in TryAddPokeballIconToHealthbox (wild encounters only, non-tutorial, non-ghost, checked against Dex).
  * Ensure status ailment icons (PSN, PAR, SLP, FRZ, BRN) take rendering priority over the caught ball marker.

- Level-Up Learnset & Movepool Assignment (fixes #2315):
  * Fix wild and generated Pokémon moveset assignment to assign the 4 most recent level-up moves up to the current level instead of getting stuck on basic moves (e.g. Tackle).

- Easy Chat / Profile Quick Chat:
  * Extract full word groups, categories, and dictionary entries from ROM.
  * Build Easy Chat interface with group selection, paginated word lists, and alphabetized filtering.
  * Fix dialog text overflowing when exiting profile / quick chat prompts.

- Nurse Joy & PC Storage / Map Polishing:
  * Implement Nurse Joy healing tray animation flow and audio cues in standard scripts.
  * Fix PC storage box header/wallpaper rendering and storage data extraction.
  * Correct Sevii Islands / Island 1 map section lookups and region map coordinate handling.
  * Fix circular dependency in battle residual handlers (partialTrap).

fixes #2325
fixes #2322
fixes #2315
fixes #2314
fixes #2313
2026-09-19 15:12:28 -05:00

596 lines
25 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Automated Test Suite for Game 3 FRLG PC & Pokémon Storage System.
local Storage = require("src.core.game3.storage")
local Bag = require("src.core.game3.bag")
local Catching = require("src.core.game3.battle/catching")
local PcMenu = require("src.ui.game3.pc_menu")
local BoxStorageUI = require("src.ui.game3.box_storage_ui")
local ReleaseSeq = require("src.ui.game3.release_seq")
local SummaryMenu = require("src.ui.game3.summary_menu")
local Dex = require("src.core.game3.dex")
local function assert_eq(a, b, msg)
if a ~= b then
error(string.format("ASSERTION FAILED: %s (expected %s, got %s)", msg or "", tostring(b), tostring(a)), 2)
end
end
local function assert_true(val, msg)
if not val then
error(string.format("ASSERTION FAILED: %s (expected true, got %s)", msg or "", tostring(val)), 2)
end
end
local function assert_false(val, msg)
if val then
error(string.format("ASSERTION FAILED: %s (expected false, got %s)", msg or "", tostring(val)), 2)
end
end
local function make_input(pressedMap)
pressedMap = pressedMap or {}
return {
wasPressed = function(self, key) return pressedMap[key] == true end,
isDown = function(self, key) return pressedMap[key] == true end,
}
end
print("=== [TEST 1] Storage Initialization (14 Boxes × 30 Slots = 420 Capacity, 50-Slot Item PC) ===")
local s = Storage.new()
assert_eq(#s.boxes, 14, "14 boxes initialized")
assert_eq(s.currentBox, 1, "Initial currentBox is 1")
assert_eq(Storage.countTotalMons(s), 0, "Initial mon count is 0")
for b = 1, 14 do
assert_eq(s.boxes[b].name, string.format("BOX %d", b), "Box name correct")
assert_eq(s.boxes[b].wallpaper, ((b - 1) % 16) + 1, "Box wallpaper initialized")
assert_eq(Storage.countBoxMons(s, b), 0, "Box starts empty")
end
assert_eq(#s.items, 1, "PC starts with 1 item (Potion)")
assert_eq(s.items[1].id, 13, "PC starts with Potion (id 13)")
assert_eq(s.items[1].qty, 1, "PC starts with 1 Potion")
print("[ok] 14 boxes and 50-slot PC initialized successfully")
print("=== [TEST 2] The PC Heal Exploit ===")
-- Create damaged, status-afflicted, low PP mon
local sickMon = {
species = 25, -- PIKACHU
level = 20,
hp = 1,
maxHp = 50,
status = "POISON",
statusAilment = "PSN",
fainted = false,
moves = {
{ id = 85, pp = 0, maxPp = 15 }, -- THUNDERBOLT
{ id = 98, pp = 2, maxPp = 30 }, -- QUICK ATTACK
},
}
local session = {
name = "RED",
party = {
{ species = 1, level = 10, hp = 30, maxHp = 30 },
sickMon,
},
storage = Storage.new(),
bag = Bag.new(),
}
-- Deposit Pikachu (party slot 2) into Box 1
local okDep, bId, sId = Storage.deposit(session, 2, 1)
assert_true(okDep, "Pikachu deposited into Box 1")
assert_eq(#session.party, 1, "Party has 1 mon left")
assert_eq(Storage.countBoxMons(session.storage, 1), 1, "Box 1 has 1 mon")
-- Verify Pikachu is completely healed
local boxedMon = Storage.getBoxMon(session.storage, 1, sId)
assert_eq(boxedMon.hp, 50, "HP fully restored to 50")
assert_eq(boxedMon.moves[1].pp, 15, "Move 1 PP fully restored to 15")
assert_eq(boxedMon.moves[2].pp, 30, "Move 2 PP fully restored to 30")
assert_eq(boxedMon.status, nil, "Poison status condition cleared")
assert_eq(boxedMon.statusAilment, nil, "Status ailment cleared")
print("[ok] PC Heal Exploit verified: HP, PP, and status fully restored upon deposit")
print("=== [TEST 3] Party Deposit / Withdraw Boundary Rules ===")
-- Prevent depositing last Pokémon
local okLast, errLast = Storage.deposit(session, 1, 1)
assert_false(okLast, "Cannot deposit last remaining party Pokémon")
assert_eq(errLast, "last_pokemon", "Error is last_pokemon")
-- Withdraw Pikachu back to party
local okWd, pIdx = Storage.withdraw(session, 1, sId)
assert_true(okWd, "Withdrew Pikachu to party")
assert_eq(#session.party, 2, "Party has 2 mons")
assert_eq(Storage.countBoxMons(session.storage, 1), 0, "Box 1 is empty again")
-- Fill party to 6 mons
for i = 3, 6 do
session.party[i] = { species = i, level = 5, hp = 20, maxHp = 20 }
end
assert_eq(#session.party, 6, "Party is full (6 mons)")
-- Deposit 1 mon to box, then try to withdraw when party is full
Storage.deposit(session, 6, 1)
assert_eq(#session.party, 5, "Party has 5 mons")
session.party[6] = { species = 99, level = 5, hp = 20, maxHp = 20 } -- refill to 6
local okWdFull, errWdFull = Storage.withdraw(session, 1, 1)
assert_false(okWdFull, "Cannot withdraw into full party")
assert_eq(errWdFull, "party_full", "Error is party_full")
print("[ok] Deposit/withdraw boundary rules passed")
print("=== [TEST 4] Circular Automatic Spillover across 14 Boxes (420 Mons) ===")
local spillSession = {
name = "ASH",
party = {
{ species = 25, level = 50, hp = 100, maxHp = 100 },
{ species = 1, level = 50, hp = 100, maxHp = 100 },
{ species = 4, level = 50, hp = 100, maxHp = 100 },
{ species = 7, level = 50, hp = 100, maxHp = 100 },
{ species = 16, level = 50, hp = 100, maxHp = 100 },
{ species = 19, level = 50, hp = 100, maxHp = 100 },
},
storage = Storage.new(),
dex = Dex.new(),
}
assert_eq(#spillSession.party, 6, "Party is full (6)")
-- Fill Box 1 completely (30 slots)
for s = 1, 30 do
spillSession.storage.boxes[1].mons[s] = { species = 10, level = 3, hp = 15, maxHp = 15 }
end
assert_eq(Storage.countBoxMons(spillSession.storage, 1), 30, "Box 1 is full (30 mons)")
-- Catch wild Caterpie (species 10) with full party and full Box 1
local foe = { species = 10, mon = { species = 10, level = 4, hp = 5, maxHp = 16 } }
local resCatch = Catching.storeCaught(spillSession, foe, 4)
assert_true(resCatch.success, "Catch successful")
assert_eq(resCatch.location, "pc", "Sent to PC storage")
assert_eq(resCatch.box, 2, "Silently spilled over to Box 2")
assert_eq(resCatch.slot, 1, "Placed in Box 2 Slot 1")
assert_eq(Storage.countBoxMons(spillSession.storage, 2), 1, "Box 2 received mon")
-- Test full 420-slot storage capacity
for b = 1, 14 do
for sl = 1, 30 do
spillSession.storage.boxes[b].mons[sl] = { species = 1, level = 5, hp = 20, maxHp = 20 }
end
end
assert_eq(Storage.countTotalMons(spillSession.storage), 420, "All 420 box slots filled")
local bOpen, sOpen = Storage.findOpenSlot(spillSession.storage)
assert_eq(bOpen, nil, "findOpenSlot returns nil when all 420 slots full")
local resFull = Catching.storeCaught(spillSession, foe, 4)
assert_false(resFull.success, "Catch rejected when all 420 slots full")
assert_eq(resFull.reason, "storage_full", "Reason is storage_full")
print("[ok] Circular automatic spillover and 420-mon cap verified")
print("=== [TEST 5] Player PC 50-Item Storage Capacity & Bag Interop ===")
local pcItemSession = {
bag = Bag.new(),
storage = Storage.new(),
}
pcItemSession.storage.items = {}
-- Give 50 Poké Balls to Bag
Bag.add(pcItemSession.bag, 4, 50) -- POKE_BALL = 4
assert_eq(Bag.get(pcItemSession.bag, 4), 50, "Bag has 50 Poké Balls")
-- Deposit 20 Poké Balls into PC
local okDepItem = Storage.depositItem(pcItemSession, "POKE_BALLS", 1, 20)
assert_true(okDepItem, "Deposited 20 Poké Balls into PC")
assert_eq(Bag.get(pcItemSession.bag, 4), 30, "Bag now has 30 Poké Balls")
assert_eq(#pcItemSession.storage.items, 1, "PC items list has 1 entry")
assert_eq(pcItemSession.storage.items[1].qty, 20, "PC holds 20 Poké Balls")
-- Withdraw 5 Poké Balls back to Bag
local okWdItem = Storage.withdrawItem(pcItemSession, 1, 5)
assert_true(okWdItem, "Withdrew 5 Poké Balls from PC")
assert_eq(Bag.get(pcItemSession.bag, 4), 35, "Bag now has 35 Poké Balls")
assert_eq(pcItemSession.storage.items[1].qty, 15, "PC holds 15 Poké Balls")
-- Toss 5 Poké Balls from PC
local okToss = Storage.tossItem(pcItemSession, 1, 5)
assert_true(okToss, "Tossed 5 Poké Balls from PC")
assert_eq(pcItemSession.storage.items[1].qty, 10, "PC holds 10 Poké Balls")
-- Test 50 unique items cap
for i = 1, 50 do
pcItemSession.storage.items[i] = { id = i, qty = 10 }
end
assert_eq(#pcItemSession.storage.items, 50, "PC holds 50 unique items")
Bag.add(pcItemSession.bag, 99, 1)
local ok51, err51 = Storage.depositItem(pcItemSession, "ITEMS", 1, 1)
assert_false(ok51, "Cannot deposit 51st unique item into PC")
assert_eq(err51, "pc_items_full", "Error is pc_items_full")
print("[ok] Player PC 50-item storage and bag interop passed")
print("=== [TEST 6] Move Items Bag-Full Protection ===")
local heldMon = {
species = 25,
level = 20,
heldItem = 13, -- POTION = 13
}
local fullBagSession = {
bag = Bag.new(),
storage = Storage.new(),
}
-- Fill Items pocket (42 items cap) with unique items from ITEMS pocket
for i = 1, 42 do
Bag.add(fullBagSession.bag, 13 + i, 1)
end
assert_false(Bag.canAdd(fullBagSession.bag, 13, 1), "Bag items pocket is completely full")
-- Attempt to detach item when bag is full
local okDetachFull, errDetachFull = Storage.detachHeldItem(fullBagSession, heldMon)
assert_false(okDetachFull, "Item detachment blocked when Bag is full")
assert_eq(errDetachFull, "bag_full", "Error reason is bag_full")
assert_eq(heldMon.heldItem, 13, "Held item was preserved on mon without being voided")
-- Make room in bag and detach
Bag.remove(fullBagSession.bag, 14, 1)
local okDetachSpace, detachedId = Storage.detachHeldItem(fullBagSession, heldMon)
assert_true(okDetachSpace, "Item successfully detached when Bag has space")
assert_eq(detachedId, 13, "Detached item was Potion")
assert_eq(heldMon.heldItem, nil, "Mon held item cleared")
assert_eq(Bag.get(fullBagSession.bag, 13), 1, "Bag received Potion")
print("[ok] Move Items bag-full protection verified")
print("=== [TEST 7] The Emotional Release Sequence Animation ===")
local relSession = {
storage = Storage.new(),
}
relSession.storage.boxes[1].mons[1] = { species = 25, level = 10, nickname = "SPARKY" }
local seqDone = false
ReleaseSeq.start({
session = relSession,
mon = relSession.storage.boxes[1].mons[1],
boxId = 1,
slotIdx = 1,
onComplete = function(released) seqDone = released end,
})
assert_true(ReleaseSeq.isActive(), "Release sequence active")
assert_eq(ReleaseSeq.state, "confirm", "Initial state is confirm")
-- Confirm YES
ReleaseSeq.handleInput(make_input({ up = true }))
assert_eq(ReleaseSeq.yesNoCursor, 1, "YES selected")
ReleaseSeq.handleInput(make_input({ a = true }))
assert_eq(ReleaseSeq.state, "anim", "Entered upward float/shrink animation state")
-- Tick animation past 0.8s
ReleaseSeq.update(1.0)
assert_eq(ReleaseSeq.state, "bye", "Entered 'Bye-bye, SPARKY!' state")
assert_eq(relSession.storage.boxes[1].mons[1], nil, "Box slot data cleared")
-- Dismiss dialogue
ReleaseSeq.handleInput(make_input({ a = true }))
assert_false(ReleaseSeq.isActive(), "Release sequence completed")
assert_true(seqDone, "Callback fired with released = true")
print("[ok] Emotional Release sequence animation state machine passed")
print("=== [TEST 8] Sparse Serialization & Deserialization Footprint ===")
local sparseStorage = Storage.new()
sparseStorage.boxes[1].mons[3] = { species = 1, level = 5 }
sparseStorage.boxes[1].mons[15] = { species = 4, level = 5 }
sparseStorage.boxes[3].mons[1] = { species = 7, level = 5 }
sparseStorage.items[1] = { id = 4, qty = 20 }
local serialized = Storage.serialize(sparseStorage)
assert_true(serialized ~= nil, "Serialized table generated")
assert_eq(serialized.boxes[2], nil, "Empty Box 2 completely omitted from serialization")
assert_eq(serialized.boxes[4], nil, "Empty Box 4 completely omitted from serialization")
assert_eq(#serialized.items, 1, "Serialized 1 item entry")
assert_true(serialized.boxes[1].mons[3] ~= nil, "Box 1 Slot 3 present")
assert_true(serialized.boxes[1].mons[15] ~= nil, "Box 1 Slot 15 present")
assert_true(serialized.boxes[3].mons[1] ~= nil, "Box 3 Slot 1 present")
-- Restore from sparse data
local deserialized = Storage.deserialize(serialized)
assert_eq(#deserialized.boxes, 14, "Deserialized all 14 boxes")
assert_eq(deserialized.boxes[1].mons[3].species, 1, "Restored Box 1 Slot 3")
assert_eq(deserialized.boxes[1].mons[15].species, 4, "Restored Box 1 Slot 15")
assert_eq(deserialized.boxes[3].mons[1].species, 7, "Restored Box 3 Slot 1")
assert_eq(Storage.countTotalMons(deserialized), 3, "Total mon count exactly 3")
print("[ok] Sparse serialization preserves lean disk footprint")
print("=== [TEST 9] Context-Aware Summary Screen Pagination in Box Mode ===")
local boxMonsList = {
{ species = 1, level = 5 },
{ species = 4, level = 5 },
{ species = 7, level = 5 },
}
SummaryMenu.openMenu(boxMonsList, 2, { context = "box" })
assert_true(SummaryMenu.isOpen(), "SummaryMenu open")
assert_eq(SummaryMenu._context, "box", "Context is box mode")
assert_eq(SummaryMenu._cursor, 2, "Cursor on Charmander (index 2)")
-- Cycle to Squirtle (index 3)
SummaryMenu.handleInput(make_input({ down = true }))
assert_eq(SummaryMenu._cursor, 3, "Navigated to Squirtle (index 3)")
SummaryMenu.close()
assert_false(SummaryMenu.isOpen(), "SummaryMenu closed cleanly")
print("[ok] Context-aware SummaryScreen in Box mode passed")
print("=== [TEST 10] BoxStorageUI Navigation & Hover Bounce ===")
local uiSession = {
party = { { species = 25, level = 10, hp = 30, maxHp = 30 } },
storage = Storage.new(),
}
uiSession.storage.boxes[1].mons[1] = { species = 1, level = 5 }
uiSession.storage.boxes[1].mons[2] = { species = 4, level = 5 }
BoxStorageUI.show({ session = uiSession })
assert_true(BoxStorageUI.isOpen(), "BoxStorageUI is open")
assert_eq(BoxStorageUI.cursorSlot, 1, "Cursor on slot 1")
assert_eq(BoxStorageUI.hoverFrame, 0, "Initial hoverFrame is 0")
-- Update hover bounce timer
BoxStorageUI.update(0.15)
assert_eq(BoxStorageUI.hoverFrame, 1, "Hover bounce toggled to frame 1")
BoxStorageUI.update(0.15)
assert_eq(BoxStorageUI.hoverFrame, 0, "Hover bounce toggled back to frame 0")
-- Navigate right to slot 2
BoxStorageUI.handleInput(make_input({ right = true }))
assert_eq(BoxStorageUI.cursorSlot, 2, "Cursor moved to slot 2")
-- Switch box with R trigger
BoxStorageUI.handleInput(make_input({ r = true }))
assert_eq(uiSession.storage.currentBox, 2, "Switched to Box 2")
BoxStorageUI.close()
assert_false(BoxStorageUI.isOpen(), "BoxStorageUI closed")
print("[ok] BoxStorageUI navigation and hover bounce passed")
print("=== [TEST 11] PcMenu Root Navigation & Submenu Lifecycle ===")
local pcSession = {
name = "RED",
flags = { [0x828] = true }, -- BILL'S PC unlocked
party = { { species = 25, level = 10, hp = 30, maxHp = 30 } },
storage = Storage.new(),
bag = Bag.new(),
}
local pcClosed = false
PcMenu.show({ session = pcSession, onClose = function() pcClosed = true end })
assert_true(PcMenu.isOpen(), "PcMenu is open")
assert_eq(PcMenu.mode, "root", "Mode is root")
-- Move to LOG OFF (index 5) and press A
for _ = 1, 4 do
PcMenu.handleInput(make_input({ down = true }))
end
assert_eq(PcMenu.cursor, 5, "Cursor on LOG OFF")
PcMenu.handleInput(make_input({ a = true }))
assert_false(PcMenu.isOpen(), "PcMenu closed")
assert_true(pcClosed, "onClose callback invoked")
print("[ok] PcMenu lifecycle passed")
print("=== [TEST 12] 1:1 Storage Submenu Flow & Validation Checks ===")
local storageSubSession = {
name = "RED",
flags = { [0x828] = true }, -- BILL'S PC
party = {
{ species = 25, level = 10, hp = 30, maxHp = 30 },
},
storage = Storage.new(),
bag = Bag.new(),
}
PcMenu.show({ session = storageSubSession })
assert_eq(PcMenu.mode, "root", "Started in root menu")
-- Select BILL'S PC (index 1)
PcMenu.handleInput(make_input({ a = true }))
assert_eq(PcMenu.mode, "storage_menu", "Entered storage_menu")
assert_eq(PcMenu.cursor, 1, "Cursor on WITHDRAW POKéMON")
assert_true(PcMenu._status:find("You can withdraw"), "Description matches WITHDRAW POKéMON")
-- Navigate down to DEPOSIT POKéMON (index 2)
PcMenu.handleInput(make_input({ down = true }))
assert_eq(PcMenu.cursor, 2, "Cursor on DEPOSIT POKéMON")
assert_true(PcMenu._status:find("You can deposit"), "Description matches DEPOSIT POKéMON")
-- Navigate down to MOVE POKéMON (index 3)
PcMenu.handleInput(make_input({ down = true }))
assert_eq(PcMenu.cursor, 3, "Cursor on MOVE POKéMON")
assert_true(PcMenu._status:find("You can move POKéMON"), "Description matches MOVE POKéMON")
-- Navigate down to MOVE ITEMS (index 4)
PcMenu.handleInput(make_input({ down = true }))
assert_eq(PcMenu.cursor, 4, "Cursor on MOVE ITEMS")
assert_true(PcMenu._status:find("You can move items"), "Description matches MOVE ITEMS")
-- Navigate down to SEE YA! (index 5)
PcMenu.handleInput(make_input({ down = true }))
assert_eq(PcMenu.cursor, 5, "Cursor on SEE YA!")
assert_true(PcMenu._status:find("See you later"), "Description matches SEE YA!")
-- Test DEPOSIT validation when party is 1
PcMenu.handleInput(make_input({ up = true, up = true, up = true })) -- index 2
PcMenu.cursor = 2
PcMenu.handleInput(make_input({ a = true }))
assert_eq(PcMenu.mode, "msg", "Blocked deposit due to single party Pokémon")
assert_eq(PcMenu._status, "Can't deposit the last POKéMON!", "Error status matches FRLG text")
PcMenu.handleInput(make_input({ a = true }))
assert_eq(PcMenu.mode, "storage_menu", "Returned to storage_menu")
-- Test WITHDRAW validation when party is full (6)
for i = 2, 6 do
storageSubSession.party[i] = { species = i, level = 5, hp = 20, maxHp = 20 }
end
assert_eq(#storageSubSession.party, 6, "Party is now full (6 mons)")
PcMenu.cursor = 1
PcMenu.handleInput(make_input({ a = true }))
assert_eq(PcMenu.mode, "msg", "Blocked withdraw due to full party")
assert_eq(PcMenu._status, "Can't take any more POKéMON.", "Error status matches FRLG text")
PcMenu.handleInput(make_input({ a = true }))
assert_eq(PcMenu.mode, "storage_menu", "Returned to storage_menu")
-- Press B to return to root PC menu
PcMenu.handleInput(make_input({ b = true }))
assert_eq(PcMenu.mode, "root", "Returned to root PC menu")
PcMenu.close()
assert_false(PcMenu.isOpen(), "PC Menu closed")
print("[ok] 1:1 Storage Submenu flow and boundary validations verified")
print("=== [TEST 13] Authentic Party Drawer Navigation & Coordinates ===")
local PcChrome = require("src.ui.game3.pc_chrome")
local BoxStorageUI = require("src.ui.game3.box_storage_ui")
-- Check GBA cursor coords
local x1, y1 = PcChrome.getPartyCursorCoords(1)
assert_eq(x1, 104, "Slot 1 (Lead) X is 104")
assert_eq(y1, 52, "Slot 1 (Lead) Y is 52")
local x2, y2 = PcChrome.getPartyCursorCoords(2)
assert_eq(x2, 152, "Slot 2 X is 152")
assert_eq(y2, 4, "Slot 2 Y is 4")
local x6, y6 = PcChrome.getPartyCursorCoords(6)
assert_eq(x6, 152, "Slot 6 X is 152")
assert_eq(y6, 100, "Slot 6 Y is 100")
local x7, y7 = PcChrome.getPartyCursorCoords(7)
assert_eq(x7, 152, "CANCEL button (Slot 7) X is 152")
assert_eq(y7, 132, "CANCEL button (Slot 7) Y is 132")
-- Test UI party drawer navigation
local partyDrawerSession = {
party = {
{ species = 1, nickname = "BULBASAUR", level = 5, hp = 20, maxHp = 20 },
{ species = 19, nickname = "RATTATA", level = 3, hp = 15, maxHp = 15 },
},
storage = Storage.new(),
}
BoxStorageUI.show({ session = partyDrawerSession })
BoxStorageUI.mode = "party_drawer"
BoxStorageUI.partyCursor = 1
-- Press Right from slot 1 moves to previous vertical slot (2)
BoxStorageUI.handleInput(make_input({ right = true }))
assert_eq(BoxStorageUI.partyCursor, 2, "Moved from Lead (1) to Slot 2")
-- Press Left from slot 2 moves back to Lead (1)
BoxStorageUI.handleInput(make_input({ left = true }))
assert_eq(BoxStorageUI.partyCursor, 1, "Moved from Slot 2 to Lead (1)")
-- Press Up from slot 1 wraps to CANCEL (7)
BoxStorageUI.handleInput(make_input({ up = true }))
assert_eq(BoxStorageUI.partyCursor, 7, "Wrapped Up from Slot 1 to CANCEL (7)")
-- Press Down from CANCEL (7) wraps to Slot 1
BoxStorageUI.handleInput(make_input({ down = true }))
assert_eq(BoxStorageUI.partyCursor, 1, "Wrapped Down from CANCEL (7) to Slot 1")
-- Press Right from slot 1 -> slot 2, then Right again exits to Box
BoxStorageUI.handleInput(make_input({ right = true }))
assert_eq(BoxStorageUI.partyCursor, 2, "On Slot 2")
BoxStorageUI.handleInput(make_input({ right = true }))
assert_eq(BoxStorageUI.mode, "browse", "Exited party drawer to Box browse mode")
assert_eq(BoxStorageUI.cursorSlot, 1, "Cursor reset to Box slot 1")
BoxStorageUI.close()
print("[ok] Party Drawer authentic navigation and coordinates verified")
print("=== [TEST 14] Party Drawer Action Menu & Drawer Visibility ===")
local partyActionSession = {
party = {
{ species = 1, nickname = "BULBASAUR", level = 5, hp = 20, maxHp = 20 },
{ species = 19, nickname = "RATTATA", level = 3, hp = 15, maxHp = 15 },
},
storage = Storage.new(),
}
BoxStorageUI.show({ session = partyActionSession, subMode = "withdraw" })
-- Navigate to PARTY POKéMON button (-10) and press A
BoxStorageUI.cursorSlot = -10
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "party_drawer", "Entered party drawer")
assert_true(BoxStorageUI.drawerOpen, "Party drawer is marked open")
-- Press A on Lead Mon (1) to open action menu
BoxStorageUI.partyCursor = 1
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "action_menu", "Action menu opened for party mon")
assert_true(BoxStorageUI.drawerOpen, "Party drawer stays open under action menu")
assert_eq(BoxStorageUI._actionSource, "party", "Action source is party")
assert_true(BoxStorageUI._actionTarget ~= nil, "Action target recorded")
assert_eq(BoxStorageUI._actionTarget.mon.nickname, "BULBASAUR", "Action target is Bulbasaur")
-- Test CANCEL from action menu returns to party drawer (drawer stays open)
BoxStorageUI.actionCursor = #BoxStorageUI._activeActions -- CANCEL
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "party_drawer", "CANCEL returned to party_drawer mode")
assert_true(BoxStorageUI.drawerOpen, "Drawer is still open")
-- Open action menu again and test STORE
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "action_menu", "Action menu re-opened")
BoxStorageUI.actionCursor = 1 -- STORE
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "party_drawer", "STORE completed and returned to party_drawer")
assert_eq(#partyActionSession.party, 1, "Party now has 1 mon")
assert_eq(partyActionSession.party[1].nickname, "RATTATA", "Remaining mon is Rattata")
assert_eq(partyActionSession.storage.boxes[1].mons[1].nickname, "BULBASAUR", "Bulbasaur stored in box 1")
-- Try to STORE last mon (Rattata) -> should show error message and return to party_drawer
BoxStorageUI.partyCursor = 1
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "action_menu", "Action menu opened on Rattata")
BoxStorageUI.actionCursor = 1 -- STORE
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "message", "Entered message mode on last mon store attempt")
assert_eq(BoxStorageUI._status, "Can't deposit the last POKéMON!", "Error status matches")
-- Dismiss message -> returns to party_drawer
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "party_drawer", "Dismissing error message returned to party_drawer")
assert_true(BoxStorageUI.drawerOpen, "Party drawer remains open")
-- Test MOVE on party mon
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "action_menu", "Action menu opened on Rattata")
BoxStorageUI.actionCursor = 3 -- MOVE
BoxStorageUI.handleInput(make_input({ a = true }))
assert_eq(BoxStorageUI.mode, "party_drawer", "MOVE returned to party_drawer")
assert_true(BoxStorageUI.holdingMon ~= nil, "Holding mon is active")
assert_eq(BoxStorageUI.holdingMon.nickname, "RATTATA", "Holding Rattata")
assert_eq(BoxStorageUI.holdingSource.loc, "party", "Holding source is party")
-- Place back down on slot 1
BoxStorageUI.handleInput(make_input({ a = true }))
assert_true(BoxStorageUI.holdingMon == nil, "Mon placed down")
assert_eq(partyActionSession.party[1].nickname, "RATTATA", "Rattata back in slot 1")
BoxStorageUI.close()
print("[ok] Party Drawer Action Menu interaction and persistence verified")
print("=== [TEST 15] Storage Chrome & Wallpapers Extraction and Manifest Integrity ===")
local StorageChromeExtract = require("src.import.gba.storage_chrome_extract")
assert_true(StorageChromeExtract.ready(), "StorageChromeExtract ready check passed")
local resExtract = StorageChromeExtract.extract(nil, { force = true })
assert_true(resExtract.ok, "StorageChromeExtract executed successfully")
assert_eq(resExtract.count, 30, "Extracted 14 UI textures + 16 box wallpapers (total 30 assets)")
local manifestChunk = assert(loadfile("data/generated/gba/pokemon/storage/manifest.lua"))
local manifest = manifestChunk()
assert_eq(manifest.version, 2, "Manifest version is 2")
assert_true(manifest.textures.cursor ~= nil, "Manifest includes cursor texture")
assert_true(manifest.textures.party_drawer_bg ~= nil, "Manifest includes party_drawer_bg")
assert_true(manifest.textures.scrolling_bg ~= nil, "Manifest includes scrolling_bg")
assert_true(manifest.wallpapers.forest ~= nil, "Manifest includes forest wallpaper")
assert_true(manifest.wallpapers.stars ~= nil, "Manifest includes stars wallpaper")
assert_true(manifest.wallpapers.simple ~= nil, "Manifest includes simple wallpaper")
for _, wpName in ipairs(PcChrome.WALLPAPER_NAMES) do
assert_true(manifest.wallpapers[wpName] ~= nil, "Wallpaper " .. wpName .. " present in manifest")
local f = io.open("data/generated/gba/pokemon/storage/wallpapers/" .. wpName .. ".png", "rb")
assert_true(f ~= nil, "Wallpaper file " .. wpName .. ".png exists on disk")
local header = f:read(8)
f:close()
assert_true(header ~= nil and header:sub(2, 4) == "PNG", "Wallpaper " .. wpName .. " is valid PNG")
end
print("[ok] All 14 UI textures and 16 wallpapers validated in manifest and file system")
print("\n========================================================")
print("ALL 15 POKÉMON STORAGE & PC SYSTEM TESTS PASSED CLEANLY!")
print("========================================================")