You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
10 KiB
359 lines
10 KiB
/** |
|
* @file inventory_ui_test.cpp |
|
* |
|
* Tests for inventory operations that are not covered by inv_test.cpp. |
|
* |
|
* Covers: AutoEquip, AutoPlaceItemInInventory, CanFitItemInInventory, |
|
* belt placement, RemoveEquipment, ReorganizeInventory, and |
|
* TransferItemToStash. |
|
*/ |
|
|
|
#include <gtest/gtest.h> |
|
|
|
#include "ui_test.hpp" |
|
|
|
#include "inv.h" |
|
#include "items.h" |
|
#include "player.h" |
|
#include "qol/stash.h" |
|
|
|
namespace devilution { |
|
namespace { |
|
|
|
// --------------------------------------------------------------------------- |
|
// Test fixture |
|
// --------------------------------------------------------------------------- |
|
|
|
class InventoryUITest : public UITest { |
|
protected: |
|
void SetUp() override |
|
{ |
|
UITest::SetUp(); |
|
|
|
// Each test starts with a completely stripped player and clean stash. |
|
StripPlayer(); |
|
Stash = {}; |
|
Stash.gold = 0; |
|
Stash.dirty = false; |
|
} |
|
|
|
// --- helpers --- |
|
|
|
/** @brief Create a sword (2×1 one-hand weapon, requires 15 Str). */ |
|
static Item MakeSword() |
|
{ |
|
Item item {}; |
|
InitializeItem(item, IDI_BARDSWORD); |
|
item._iIdentified = true; |
|
return item; |
|
} |
|
|
|
/** @brief Create a healing potion (1×1, beltable). */ |
|
static Item MakePotion() |
|
{ |
|
Item item {}; |
|
InitializeItem(item, IDI_HEAL); |
|
return item; |
|
} |
|
|
|
/** @brief Create a short staff (1×3 two-hand weapon, requires 0 Str). */ |
|
static Item MakeStaff() |
|
{ |
|
Item item {}; |
|
InitializeItem(item, IDI_SHORTSTAFF); |
|
item._iIdentified = true; |
|
return item; |
|
} |
|
|
|
/** |
|
* @brief Fill the entire inventory grid with 1×1 healing potions. |
|
* |
|
* After this call every one of the 40 inventory cells is occupied. |
|
*/ |
|
void FillInventory() |
|
{ |
|
for (int i = 0; i < InventoryGridCells; i++) { |
|
Item potion = MakePotion(); |
|
ASSERT_TRUE(AutoPlaceItemInInventory(*MyPlayer, potion)) |
|
<< "Failed to place potion at cell " << i; |
|
} |
|
} |
|
|
|
/** @brief Fill all 8 belt slots with healing potions. */ |
|
void FillBelt() |
|
{ |
|
for (int i = 0; i < MaxBeltItems; i++) { |
|
Item potion = MakePotion(); |
|
ASSERT_TRUE(AutoPlaceItemInBelt(*MyPlayer, potion, /*persistItem=*/true)) |
|
<< "Failed to place potion in belt slot " << i; |
|
} |
|
} |
|
}; |
|
|
|
// =========================================================================== |
|
// AutoEquip tests |
|
// =========================================================================== |
|
|
|
TEST_F(InventoryUITest, AutoEquip_SwordGoesToHand) |
|
{ |
|
Item sword = MakeSword(); |
|
sword._iStatFlag = true; // Player meets stat requirements |
|
|
|
bool equipped = AutoEquip(*MyPlayer, sword); |
|
|
|
EXPECT_TRUE(equipped) << "AutoEquip should succeed for a usable sword"; |
|
EXPECT_FALSE(MyPlayer->InvBody[INVLOC_HAND_LEFT].isEmpty()) |
|
<< "Sword should be placed in the left hand slot"; |
|
EXPECT_EQ(MyPlayer->InvBody[INVLOC_HAND_LEFT].IDidx, sword.IDidx) |
|
<< "Equipped item should match the sword we created"; |
|
} |
|
|
|
TEST_F(InventoryUITest, AutoEquip_ReturnsFalseForUnusableItem) |
|
{ |
|
Item sword = MakeSword(); |
|
sword._iStatFlag = false; // Player does NOT meet stat requirements |
|
|
|
bool equipped = AutoEquip(*MyPlayer, sword); |
|
|
|
EXPECT_FALSE(equipped) |
|
<< "AutoEquip should return false when _iStatFlag is false"; |
|
EXPECT_TRUE(MyPlayer->InvBody[INVLOC_HAND_LEFT].isEmpty()) |
|
<< "Left hand slot should remain empty"; |
|
EXPECT_TRUE(MyPlayer->InvBody[INVLOC_HAND_RIGHT].isEmpty()) |
|
<< "Right hand slot should remain empty"; |
|
} |
|
|
|
TEST_F(InventoryUITest, AutoEquip_FailsWhenSlotOccupied) |
|
{ |
|
// Equip the first sword. |
|
Item sword1 = MakeSword(); |
|
sword1._iStatFlag = true; |
|
ASSERT_TRUE(AutoEquip(*MyPlayer, sword1)); |
|
|
|
// Also fill the right hand so neither hand slot is free. |
|
Item sword2 = MakeSword(); |
|
sword2._iStatFlag = true; |
|
MyPlayer->InvBody[INVLOC_HAND_RIGHT] = sword2; |
|
|
|
// Now try to auto-equip a third sword — both hand slots are occupied, |
|
// so CanEquip should reject it (AutoEquip does NOT swap). |
|
Item sword3 = MakeSword(); |
|
sword3._iStatFlag = true; |
|
|
|
bool equipped = AutoEquip(*MyPlayer, sword3); |
|
|
|
EXPECT_FALSE(equipped) |
|
<< "AutoEquip should return false when all valid body slots are occupied"; |
|
} |
|
|
|
// =========================================================================== |
|
// AutoPlaceItemInInventory / CanFitItemInInventory tests |
|
// =========================================================================== |
|
|
|
TEST_F(InventoryUITest, AutoPlaceItem_EmptyInventory) |
|
{ |
|
Item sword = MakeSword(); |
|
|
|
bool placed = AutoPlaceItemInInventory(*MyPlayer, sword); |
|
|
|
EXPECT_TRUE(placed) << "Should be able to place an item in an empty inventory"; |
|
EXPECT_EQ(MyPlayer->_pNumInv, 1) |
|
<< "Inventory count should be 1 after placing one item"; |
|
|
|
// Verify the item is actually stored. |
|
EXPECT_EQ(MyPlayer->InvList[0].IDidx, sword.IDidx) |
|
<< "InvList[0] should contain the sword"; |
|
|
|
// Verify at least one grid cell is set (non-zero). |
|
bool gridSet = false; |
|
for (int i = 0; i < InventoryGridCells; i++) { |
|
if (MyPlayer->InvGrid[i] != 0) { |
|
gridSet = true; |
|
break; |
|
} |
|
} |
|
EXPECT_TRUE(gridSet) << "At least one InvGrid cell should be non-zero"; |
|
} |
|
|
|
TEST_F(InventoryUITest, AutoPlaceItem_FullInventory) |
|
{ |
|
FillInventory(); |
|
|
|
// Inventory is full — a new item should not fit. |
|
Item extraPotion = MakePotion(); |
|
bool placed = AutoPlaceItemInInventory(*MyPlayer, extraPotion); |
|
|
|
EXPECT_FALSE(placed) |
|
<< "Should not be able to place an item in a completely full inventory"; |
|
} |
|
|
|
TEST_F(InventoryUITest, CanFitItem_EmptyInventory) |
|
{ |
|
Item sword = MakeSword(); |
|
|
|
EXPECT_TRUE(CanFitItemInInventory(*MyPlayer, sword)) |
|
<< "An empty inventory should have room for a sword"; |
|
} |
|
|
|
TEST_F(InventoryUITest, CanFitItem_FullInventory) |
|
{ |
|
FillInventory(); |
|
|
|
Item extraPotion = MakePotion(); |
|
|
|
EXPECT_FALSE(CanFitItemInInventory(*MyPlayer, extraPotion)) |
|
<< "A completely full inventory should report no room"; |
|
} |
|
|
|
// =========================================================================== |
|
// Belt tests |
|
// =========================================================================== |
|
|
|
TEST_F(InventoryUITest, CanBePlacedOnBelt_Potion) |
|
{ |
|
Item potion = MakePotion(); |
|
|
|
EXPECT_TRUE(CanBePlacedOnBelt(*MyPlayer, potion)) |
|
<< "A healing potion (1×1) should be placeable on the belt"; |
|
} |
|
|
|
TEST_F(InventoryUITest, CanBePlacedOnBelt_Sword) |
|
{ |
|
Item sword = MakeSword(); |
|
|
|
EXPECT_FALSE(CanBePlacedOnBelt(*MyPlayer, sword)) |
|
<< "A sword (2×1 weapon) should NOT be placeable on the belt"; |
|
} |
|
|
|
TEST_F(InventoryUITest, AutoPlaceBelt_Success) |
|
{ |
|
Item potion = MakePotion(); |
|
|
|
bool placed = AutoPlaceItemInBelt(*MyPlayer, potion, /*persistItem=*/true); |
|
|
|
EXPECT_TRUE(placed) << "Should be able to place a potion in an empty belt"; |
|
|
|
// Verify at least one belt slot contains the item. |
|
bool found = false; |
|
for (int i = 0; i < MaxBeltItems; i++) { |
|
if (!MyPlayer->SpdList[i].isEmpty()) { |
|
found = true; |
|
break; |
|
} |
|
} |
|
EXPECT_TRUE(found) << "Potion should appear in one of the belt slots"; |
|
} |
|
|
|
TEST_F(InventoryUITest, AutoPlaceBelt_Full) |
|
{ |
|
FillBelt(); |
|
|
|
Item extraPotion = MakePotion(); |
|
bool placed = AutoPlaceItemInBelt(*MyPlayer, extraPotion, /*persistItem=*/true); |
|
|
|
EXPECT_FALSE(placed) |
|
<< "Should not be able to place a potion when all 8 belt slots are occupied"; |
|
} |
|
|
|
// =========================================================================== |
|
// RemoveEquipment test |
|
// =========================================================================== |
|
|
|
TEST_F(InventoryUITest, RemoveEquipment_ClearsBodySlot) |
|
{ |
|
// Equip a sword in the left hand. |
|
Item sword = MakeSword(); |
|
sword._iStatFlag = true; |
|
ASSERT_TRUE(AutoEquip(*MyPlayer, sword)); |
|
ASSERT_FALSE(MyPlayer->InvBody[INVLOC_HAND_LEFT].isEmpty()) |
|
<< "Precondition: left hand should have the sword"; |
|
|
|
RemoveEquipment(*MyPlayer, INVLOC_HAND_LEFT, false); |
|
|
|
EXPECT_TRUE(MyPlayer->InvBody[INVLOC_HAND_LEFT].isEmpty()) |
|
<< "Left hand slot should be empty after RemoveEquipment"; |
|
} |
|
|
|
// =========================================================================== |
|
// ReorganizeInventory test |
|
// =========================================================================== |
|
|
|
TEST_F(InventoryUITest, ReorganizeInventory_DefragmentsGrid) |
|
{ |
|
// Place three potions via AutoPlace so the grid is properly populated. |
|
Item p1 = MakePotion(); |
|
Item p2 = MakePotion(); |
|
Item p3 = MakePotion(); |
|
|
|
ASSERT_TRUE(AutoPlaceItemInInventory(*MyPlayer, p1)); |
|
ASSERT_TRUE(AutoPlaceItemInInventory(*MyPlayer, p2)); |
|
ASSERT_TRUE(AutoPlaceItemInInventory(*MyPlayer, p3)); |
|
ASSERT_EQ(MyPlayer->_pNumInv, 3); |
|
|
|
// Remove the middle item to create a gap. |
|
MyPlayer->RemoveInvItem(1); |
|
ASSERT_EQ(MyPlayer->_pNumInv, 2); |
|
|
|
// Reorganize should keep all remaining items and defragment the grid. |
|
ReorganizeInventory(*MyPlayer); |
|
|
|
EXPECT_EQ(MyPlayer->_pNumInv, 2) |
|
<< "Item count should be preserved after reorganization"; |
|
|
|
// After reorganization, a potion should still fit (there are 38 free cells). |
|
Item extra = MakePotion(); |
|
EXPECT_TRUE(CanFitItemInInventory(*MyPlayer, extra)) |
|
<< "Should be able to fit another item after reorganization"; |
|
|
|
// Verify no InvList entries in the active range are empty. |
|
for (int i = 0; i < MyPlayer->_pNumInv; i++) { |
|
EXPECT_FALSE(MyPlayer->InvList[i].isEmpty()) |
|
<< "InvList[" << i << "] should not be empty within active range"; |
|
} |
|
} |
|
|
|
// =========================================================================== |
|
// TransferItemToStash tests |
|
// =========================================================================== |
|
|
|
TEST_F(InventoryUITest, TransferToStash_FromInventory) |
|
{ |
|
IsStashOpen = true; |
|
|
|
// Place a sword in the inventory. |
|
Item sword = MakeSword(); |
|
int idx = PlaceItemInInventory(sword); |
|
ASSERT_GE(idx, 0) << "Failed to place sword in inventory"; |
|
ASSERT_EQ(MyPlayer->_pNumInv, 1); |
|
|
|
int invLocation = INVITEM_INV_FIRST + idx; |
|
|
|
TransferItemToStash(*MyPlayer, invLocation); |
|
|
|
// Item should now be in the stash. |
|
EXPECT_FALSE(Stash.stashList.empty()) |
|
<< "Stash should contain the transferred item"; |
|
|
|
// Item should be removed from inventory. |
|
EXPECT_EQ(MyPlayer->_pNumInv, 0) |
|
<< "Inventory should be empty after transferring the only item"; |
|
} |
|
|
|
TEST_F(InventoryUITest, TransferToStash_InvalidLocation) |
|
{ |
|
IsStashOpen = true; |
|
|
|
size_t stashSizeBefore = Stash.stashList.size(); |
|
int invCountBefore = MyPlayer->_pNumInv; |
|
|
|
// Passing -1 should be a no-op (early return), not a crash. |
|
TransferItemToStash(*MyPlayer, -1); |
|
|
|
EXPECT_EQ(Stash.stashList.size(), stashSizeBefore) |
|
<< "Stash should be unchanged after invalid transfer"; |
|
EXPECT_EQ(MyPlayer->_pNumInv, invCountBefore) |
|
<< "Inventory should be unchanged after invalid transfer"; |
|
} |
|
|
|
} // namespace |
|
} // namespace devilution
|