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.
461 lines
16 KiB
461 lines
16 KiB
|
22 hours ago
|
/**
|
||
|
|
* @file spell_ui_test.cpp
|
||
|
|
*
|
||
|
|
* Tests for the spell book and spell list UI functionality.
|
||
|
|
*
|
||
|
|
* Covers:
|
||
|
|
* - GetSpellListItems() returning learned spells, abilities, and scroll spells
|
||
|
|
* - SetSpell() changing the player's active/readied spell
|
||
|
|
* - SetSpeedSpell() assigning spells to hotkey slots
|
||
|
|
* - IsValidSpeedSpell() validating hotkey slot assignments
|
||
|
|
* - DoSpeedBook() opening the speed spell selection overlay
|
||
|
|
* - ToggleSpell() cycling through available spell types for a hotkey
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include <gtest/gtest.h>
|
||
|
|
|
||
|
|
#include "ui_test.hpp"
|
||
|
|
|
||
|
|
#include "control/control.hpp"
|
||
|
|
#include "diablo.h"
|
||
|
|
#include "panels/spell_icons.hpp"
|
||
|
|
#include "panels/spell_list.hpp"
|
||
|
|
#include "player.h"
|
||
|
|
#include "spells.h"
|
||
|
|
#include "tables/spelldat.h"
|
||
|
|
|
||
|
|
namespace devilution {
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Test fixture for spell UI tests.
|
||
|
|
*
|
||
|
|
* Inherits from UITest which provides a fully-initialised single-player game
|
||
|
|
* with a level-25 Warrior, 100,000 gold, all panels closed, loopback
|
||
|
|
* networking, and HeadlessMode enabled.
|
||
|
|
*/
|
||
|
|
class SpellUITest : public UITest {
|
||
|
|
protected:
|
||
|
|
void SetUp() override
|
||
|
|
{
|
||
|
|
UITest::SetUp();
|
||
|
|
|
||
|
|
// Ensure all hotkey slots start invalid so tests are deterministic.
|
||
|
|
for (size_t i = 0; i < NumHotkeys; ++i) {
|
||
|
|
MyPlayer->_pSplHotKey[i] = SpellID::Invalid;
|
||
|
|
MyPlayer->_pSplTHotKey[i] = SpellType::Invalid;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Teach the player a memorised spell at the given level.
|
||
|
|
*
|
||
|
|
* Sets the appropriate bit in _pMemSpells and assigns a spell level
|
||
|
|
* so the spell is usable (level > 0).
|
||
|
|
*/
|
||
|
|
static void TeachSpell(SpellID spell, uint8_t level = 5)
|
||
|
|
{
|
||
|
|
MyPlayer->_pMemSpells |= GetSpellBitmask(spell);
|
||
|
|
MyPlayer->_pSplLvl[static_cast<int8_t>(spell)] = level;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Add a scroll-type spell to the player's available spells.
|
||
|
|
*
|
||
|
|
* Sets the appropriate bit in _pScrlSpells. Note that for the spell
|
||
|
|
* to actually be castable the player would need a scroll item, but
|
||
|
|
* for UI listing purposes the bitmask is sufficient.
|
||
|
|
*/
|
||
|
|
static void AddScrollSpell(SpellID spell)
|
||
|
|
{
|
||
|
|
MyPlayer->_pScrlSpells |= GetSpellBitmask(spell);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Open the speed book overlay.
|
||
|
|
*
|
||
|
|
* Sets SpellSelectFlag = true and positions the mouse via DoSpeedBook().
|
||
|
|
* DoSpeedBook() internally calls SetCursorPos() which, because
|
||
|
|
* ControlDevice defaults to ControlTypes::None (not KeyboardAndMouse),
|
||
|
|
* simply writes to MousePosition without touching SDL windowing.
|
||
|
|
*/
|
||
|
|
static void OpenSpeedBook()
|
||
|
|
{
|
||
|
|
DoSpeedBook();
|
||
|
|
// DoSpeedBook sets SpellSelectFlag = true.
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Search the spell list for an item matching the given spell ID and type.
|
||
|
|
* @return Pointer to the matching SpellListItem, or nullptr if not found.
|
||
|
|
*/
|
||
|
|
static const SpellListItem *FindInSpellList(
|
||
|
|
const std::vector<SpellListItem> &items,
|
||
|
|
SpellID id,
|
||
|
|
SpellType type)
|
||
|
|
{
|
||
|
|
for (const auto &item : items) {
|
||
|
|
if (item.id == id && item.type == type)
|
||
|
|
return &item;
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Find a spell list item by ID only (any type).
|
||
|
|
*/
|
||
|
|
static const SpellListItem *FindInSpellListById(
|
||
|
|
const std::vector<SpellListItem> &items,
|
||
|
|
SpellID id)
|
||
|
|
{
|
||
|
|
for (const auto &item : items) {
|
||
|
|
if (item.id == id)
|
||
|
|
return &item;
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Position the mouse over a spell list item so that
|
||
|
|
* GetSpellListSelection() will consider it "selected".
|
||
|
|
*
|
||
|
|
* The spell list item's `location` field gives the bottom-left corner
|
||
|
|
* of the icon. The icon occupies a SPLICONLENGTH x SPLICONLENGTH area
|
||
|
|
* from (location.x, location.y - SPLICONLENGTH) to
|
||
|
|
* (location.x + SPLICONLENGTH - 1, location.y - 1).
|
||
|
|
* We position the mouse in the centre of that area.
|
||
|
|
*/
|
||
|
|
static void PositionMouseOver(const SpellListItem &item)
|
||
|
|
{
|
||
|
|
// The selection check in GetSpellListItems() is:
|
||
|
|
// MousePosition.x >= lx && MousePosition.x < lx + SPLICONLENGTH
|
||
|
|
// MousePosition.y >= ly && MousePosition.y < ly + SPLICONLENGTH
|
||
|
|
// where lx = item.location.x, ly = item.location.y - SPLICONLENGTH
|
||
|
|
MousePosition = Point { item.location.x + SPLICONLENGTH / 2,
|
||
|
|
item.location.y - SPLICONLENGTH / 2 };
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: GetSpellListItems returns learned (memorised) spells
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, GetSpellListItems_ReturnsLearnedSpells)
|
||
|
|
{
|
||
|
|
// Teach the player Firebolt as a memorised spell.
|
||
|
|
TeachSpell(SpellID::Firebolt, 5);
|
||
|
|
|
||
|
|
// Open the speed book so GetSpellListItems() has the right context.
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
const auto items = GetSpellListItems();
|
||
|
|
|
||
|
|
// The list should contain Firebolt with SpellType::Spell.
|
||
|
|
const SpellListItem *found = FindInSpellList(items, SpellID::Firebolt, SpellType::Spell);
|
||
|
|
ASSERT_NE(found, nullptr)
|
||
|
|
<< "Firebolt should appear in the spell list after being taught";
|
||
|
|
EXPECT_EQ(found->id, SpellID::Firebolt);
|
||
|
|
EXPECT_EQ(found->type, SpellType::Spell);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: GetSpellListItems includes the Warrior's innate abilities
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, GetSpellListItems_IncludesAbilities)
|
||
|
|
{
|
||
|
|
// After CreatePlayer() for a Warrior, _pAblSpells should include the
|
||
|
|
// Warrior's skill (ItemRepair). Verify it appears in the spell list.
|
||
|
|
ASSERT_NE(MyPlayer->_pAblSpells, 0u)
|
||
|
|
<< "Warrior should have at least one ability after CreatePlayer()";
|
||
|
|
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
const auto items = GetSpellListItems();
|
||
|
|
|
||
|
|
// The Warrior's skill is ItemRepair (loaded from starting_loadout.tsv).
|
||
|
|
const SpellListItem *found = FindInSpellList(items, SpellID::ItemRepair, SpellType::Skill);
|
||
|
|
EXPECT_NE(found, nullptr)
|
||
|
|
<< "Warrior's ItemRepair ability should appear in the spell list";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: GetSpellListItems includes scroll spells
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, GetSpellListItems_IncludesScrollSpells)
|
||
|
|
{
|
||
|
|
// Give the player a Town Portal scroll spell via the bitmask.
|
||
|
|
AddScrollSpell(SpellID::TownPortal);
|
||
|
|
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
const auto items = GetSpellListItems();
|
||
|
|
|
||
|
|
const SpellListItem *found = FindInSpellList(items, SpellID::TownPortal, SpellType::Scroll);
|
||
|
|
ASSERT_NE(found, nullptr)
|
||
|
|
<< "TownPortal should appear in the spell list as a scroll spell";
|
||
|
|
EXPECT_EQ(found->type, SpellType::Scroll);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: GetSpellListItems is empty when all spell bitmasks are cleared
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, GetSpellListItems_EmptyWhenAllSpellsCleared)
|
||
|
|
{
|
||
|
|
// Clear every spell bitmask, including abilities.
|
||
|
|
MyPlayer->_pMemSpells = 0;
|
||
|
|
MyPlayer->_pAblSpells = 0;
|
||
|
|
MyPlayer->_pScrlSpells = 0;
|
||
|
|
MyPlayer->_pISpells = 0;
|
||
|
|
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
const auto items = GetSpellListItems();
|
||
|
|
|
||
|
|
EXPECT_TRUE(items.empty())
|
||
|
|
<< "Spell list should be empty when all spell bitmasks are zero";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: SetSpell changes the player's active/readied spell
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, SetSpell_ChangesActiveSpell)
|
||
|
|
{
|
||
|
|
// Teach the player Firebolt.
|
||
|
|
TeachSpell(SpellID::Firebolt, 5);
|
||
|
|
|
||
|
|
// Open speed book — this sets SpellSelectFlag and positions the mouse.
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
// Get the spell list and find Firebolt's icon position.
|
||
|
|
auto items = GetSpellListItems();
|
||
|
|
const SpellListItem *firebolt = FindInSpellList(items, SpellID::Firebolt, SpellType::Spell);
|
||
|
|
ASSERT_NE(firebolt, nullptr)
|
||
|
|
<< "Firebolt must be in the spell list for SetSpell to work";
|
||
|
|
|
||
|
|
// Position the mouse over Firebolt's icon so it becomes "selected".
|
||
|
|
PositionMouseOver(*firebolt);
|
||
|
|
|
||
|
|
// Re-fetch items to confirm the selection is detected.
|
||
|
|
items = GetSpellListItems();
|
||
|
|
const SpellListItem *selected = FindInSpellList(items, SpellID::Firebolt, SpellType::Spell);
|
||
|
|
ASSERT_NE(selected, nullptr);
|
||
|
|
EXPECT_TRUE(selected->isSelected)
|
||
|
|
<< "Firebolt should be selected after positioning mouse over it";
|
||
|
|
|
||
|
|
// Now call SetSpell — should set the player's readied spell.
|
||
|
|
SetSpell();
|
||
|
|
|
||
|
|
EXPECT_EQ(MyPlayer->_pRSpell, SpellID::Firebolt)
|
||
|
|
<< "Active spell should be Firebolt after SetSpell()";
|
||
|
|
EXPECT_EQ(MyPlayer->_pRSplType, SpellType::Spell)
|
||
|
|
<< "Active spell type should be Spell after SetSpell()";
|
||
|
|
|
||
|
|
// SetSpell also clears SpellSelectFlag.
|
||
|
|
EXPECT_FALSE(SpellSelectFlag)
|
||
|
|
<< "SpellSelectFlag should be cleared after SetSpell()";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: SetSpeedSpell assigns a spell to a hotkey slot
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, SetSpeedSpell_AssignsHotkey)
|
||
|
|
{
|
||
|
|
// Teach the player Firebolt.
|
||
|
|
TeachSpell(SpellID::Firebolt, 5);
|
||
|
|
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
// Find Firebolt's position and move the mouse there.
|
||
|
|
auto items = GetSpellListItems();
|
||
|
|
const SpellListItem *firebolt = FindInSpellList(items, SpellID::Firebolt, SpellType::Spell);
|
||
|
|
ASSERT_NE(firebolt, nullptr);
|
||
|
|
|
||
|
|
PositionMouseOver(*firebolt);
|
||
|
|
|
||
|
|
// Assign to hotkey slot 0.
|
||
|
|
SetSpeedSpell(0);
|
||
|
|
|
||
|
|
// Verify the hotkey was assigned.
|
||
|
|
EXPECT_TRUE(IsValidSpeedSpell(0))
|
||
|
|
<< "Hotkey slot 0 should be valid after assigning Firebolt";
|
||
|
|
EXPECT_EQ(MyPlayer->_pSplHotKey[0], SpellID::Firebolt)
|
||
|
|
<< "Hotkey slot 0 should contain Firebolt";
|
||
|
|
EXPECT_EQ(MyPlayer->_pSplTHotKey[0], SpellType::Spell)
|
||
|
|
<< "Hotkey slot 0 type should be Spell";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: IsValidSpeedSpell returns false for an unassigned slot
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, IsValidSpeedSpell_InvalidSlot)
|
||
|
|
{
|
||
|
|
// Slot 0 was cleared to SpellID::Invalid in SetUp().
|
||
|
|
EXPECT_FALSE(IsValidSpeedSpell(0))
|
||
|
|
<< "Unassigned hotkey slot should not be valid";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: DoSpeedBook opens the spell selection overlay
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, DoSpeedBook_OpensSpellSelect)
|
||
|
|
{
|
||
|
|
// Ensure it's closed initially.
|
||
|
|
ASSERT_FALSE(SpellSelectFlag);
|
||
|
|
|
||
|
|
DoSpeedBook();
|
||
|
|
|
||
|
|
EXPECT_TRUE(SpellSelectFlag)
|
||
|
|
<< "SpellSelectFlag should be true after DoSpeedBook()";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: SpellSelectFlag can be toggled off (simulating closing the speed book)
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, DoSpeedBook_ClosesSpellSelect)
|
||
|
|
{
|
||
|
|
// Open the speed book.
|
||
|
|
DoSpeedBook();
|
||
|
|
ASSERT_TRUE(SpellSelectFlag);
|
||
|
|
|
||
|
|
// Simulate closing by clearing the flag (this is what the key handler does).
|
||
|
|
SpellSelectFlag = false;
|
||
|
|
|
||
|
|
EXPECT_FALSE(SpellSelectFlag)
|
||
|
|
<< "SpellSelectFlag should be false after being manually cleared";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: ToggleSpell cycles through available spell types for a hotkey
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, ToggleSpell_CyclesThroughTypes)
|
||
|
|
{
|
||
|
|
// Set up a spell that is available as both a memorised spell and a scroll.
|
||
|
|
// Using Firebolt for this test.
|
||
|
|
TeachSpell(SpellID::Firebolt, 5);
|
||
|
|
AddScrollSpell(SpellID::Firebolt);
|
||
|
|
|
||
|
|
// Assign Firebolt (as Spell type) to hotkey slot 0.
|
||
|
|
MyPlayer->_pSplHotKey[0] = SpellID::Firebolt;
|
||
|
|
MyPlayer->_pSplTHotKey[0] = SpellType::Spell;
|
||
|
|
|
||
|
|
ASSERT_TRUE(IsValidSpeedSpell(0))
|
||
|
|
<< "Hotkey slot 0 should be valid with Firebolt as Spell";
|
||
|
|
|
||
|
|
// ToggleSpell activates the spell from the hotkey — it sets the player's
|
||
|
|
// readied spell to whatever is in the hotkey slot.
|
||
|
|
ToggleSpell(0);
|
||
|
|
|
||
|
|
// After ToggleSpell, the player's readied spell should match the hotkey.
|
||
|
|
EXPECT_EQ(MyPlayer->_pRSpell, SpellID::Firebolt);
|
||
|
|
EXPECT_EQ(MyPlayer->_pRSplType, SpellType::Spell);
|
||
|
|
|
||
|
|
// Now change the hotkey to Scroll type and toggle again.
|
||
|
|
MyPlayer->_pSplTHotKey[0] = SpellType::Scroll;
|
||
|
|
ASSERT_TRUE(IsValidSpeedSpell(0))
|
||
|
|
<< "Hotkey slot 0 should be valid with Firebolt as Scroll";
|
||
|
|
|
||
|
|
ToggleSpell(0);
|
||
|
|
|
||
|
|
EXPECT_EQ(MyPlayer->_pRSpell, SpellID::Firebolt);
|
||
|
|
EXPECT_EQ(MyPlayer->_pRSplType, SpellType::Scroll)
|
||
|
|
<< "After toggling with Scroll type, readied spell type should be Scroll";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: SetSpeedSpell unsets a hotkey when called with the same spell
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, SetSpeedSpell_UnsetsOnDoubleAssign)
|
||
|
|
{
|
||
|
|
// Teach the player Firebolt.
|
||
|
|
TeachSpell(SpellID::Firebolt, 5);
|
||
|
|
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
auto items = GetSpellListItems();
|
||
|
|
const SpellListItem *firebolt = FindInSpellList(items, SpellID::Firebolt, SpellType::Spell);
|
||
|
|
ASSERT_NE(firebolt, nullptr);
|
||
|
|
PositionMouseOver(*firebolt);
|
||
|
|
|
||
|
|
// Assign to slot 0 the first time.
|
||
|
|
SetSpeedSpell(0);
|
||
|
|
ASSERT_TRUE(IsValidSpeedSpell(0));
|
||
|
|
ASSERT_EQ(MyPlayer->_pSplHotKey[0], SpellID::Firebolt);
|
||
|
|
|
||
|
|
// Re-fetch items and re-position mouse (SetSpeedSpell doesn't move the cursor).
|
||
|
|
items = GetSpellListItems();
|
||
|
|
firebolt = FindInSpellList(items, SpellID::Firebolt, SpellType::Spell);
|
||
|
|
ASSERT_NE(firebolt, nullptr);
|
||
|
|
PositionMouseOver(*firebolt);
|
||
|
|
|
||
|
|
// Assign to slot 0 again — should unset (toggle off).
|
||
|
|
SetSpeedSpell(0);
|
||
|
|
EXPECT_EQ(MyPlayer->_pSplHotKey[0], SpellID::Invalid)
|
||
|
|
<< "Assigning the same spell to the same slot should unset the hotkey";
|
||
|
|
EXPECT_FALSE(IsValidSpeedSpell(0))
|
||
|
|
<< "Hotkey slot should be invalid after being unset";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: IsValidSpeedSpell returns false when the spell is no longer available
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, IsValidSpeedSpell_InvalidAfterSpellRemoved)
|
||
|
|
{
|
||
|
|
// Teach and assign Firebolt to slot 0.
|
||
|
|
TeachSpell(SpellID::Firebolt, 5);
|
||
|
|
MyPlayer->_pSplHotKey[0] = SpellID::Firebolt;
|
||
|
|
MyPlayer->_pSplTHotKey[0] = SpellType::Spell;
|
||
|
|
ASSERT_TRUE(IsValidSpeedSpell(0));
|
||
|
|
|
||
|
|
// Remove the spell from the player's memory.
|
||
|
|
MyPlayer->_pMemSpells &= ~GetSpellBitmask(SpellID::Firebolt);
|
||
|
|
|
||
|
|
// The hotkey still points to Firebolt, but the player no longer knows it.
|
||
|
|
EXPECT_FALSE(IsValidSpeedSpell(0))
|
||
|
|
<< "Hotkey should be invalid when the underlying spell is no longer available";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Test: Multiple spells appear in the spell list simultaneously
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
TEST_F(SpellUITest, GetSpellListItems_MultipleSpells)
|
||
|
|
{
|
||
|
|
// Teach multiple spells.
|
||
|
|
TeachSpell(SpellID::Firebolt, 3);
|
||
|
|
TeachSpell(SpellID::HealOther, 2);
|
||
|
|
AddScrollSpell(SpellID::TownPortal);
|
||
|
|
|
||
|
|
OpenSpeedBook();
|
||
|
|
|
||
|
|
const auto items = GetSpellListItems();
|
||
|
|
|
||
|
|
// Verify all three appear (plus the Warrior's innate ability).
|
||
|
|
EXPECT_NE(FindInSpellList(items, SpellID::Firebolt, SpellType::Spell), nullptr)
|
||
|
|
<< "Firebolt (memorised) should be in the list";
|
||
|
|
EXPECT_NE(FindInSpellList(items, SpellID::HealOther, SpellType::Spell), nullptr)
|
||
|
|
<< "HealOther (memorised) should be in the list";
|
||
|
|
EXPECT_NE(FindInSpellList(items, SpellID::TownPortal, SpellType::Scroll), nullptr)
|
||
|
|
<< "TownPortal (scroll) should be in the list";
|
||
|
|
EXPECT_NE(FindInSpellList(items, SpellID::ItemRepair, SpellType::Skill), nullptr)
|
||
|
|
<< "Warrior's ItemRepair ability should still be present";
|
||
|
|
|
||
|
|
// We should have at least 4 items.
|
||
|
|
EXPECT_GE(items.size(), 4u);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
} // namespace devilution
|