feat(api): add ItemRarity enum to item system

- Add ItemRarity enum with 5 tiers (common, uncommon, rare, epic, legendary)
- Add rarity field to Item dataclass with COMMON default
- Update Item serialization (to_dict/from_dict) for rarity
- Export ItemRarity from models package
- Add 24 comprehensive unit tests for Item and ItemRarity

Part of Phase 4 Week 2: Inventory & Equipment System (Task 2.1)
This commit is contained in:
2025-11-26 16:14:29 -06:00
parent 03ab783eeb
commit f3ac0c8647
4 changed files with 407 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ from app.models.enums import (
EffectType,
DamageType,
ItemType,
ItemRarity,
StatType,
AbilityType,
CombatStatus,
@@ -53,6 +54,7 @@ __all__ = [
"EffectType",
"DamageType",
"ItemType",
"ItemRarity",
"StatType",
"AbilityType",
"CombatStatus",

View File

@@ -40,6 +40,16 @@ class ItemType(Enum):
QUEST_ITEM = "quest_item" # Story-related, non-tradeable
class ItemRarity(Enum):
"""Item rarity tiers affecting drop rates, value, and visual styling."""
COMMON = "common" # White/gray - basic items
UNCOMMON = "uncommon" # Green - slightly better
RARE = "rare" # Blue - noticeably better
EPIC = "epic" # Purple - powerful items
LEGENDARY = "legendary" # Orange/gold - best items
class StatType(Enum):
"""Character attribute types."""

View File

@@ -8,7 +8,7 @@ including weapons, armor, consumables, and quest items.
from dataclasses import dataclass, field, asdict
from typing import Dict, Any, List, Optional
from app.models.enums import ItemType, DamageType
from app.models.enums import ItemType, ItemRarity, DamageType
from app.models.effects import Effect
@@ -24,6 +24,7 @@ class Item:
item_id: Unique identifier
name: Display name
item_type: Category (weapon, armor, consumable, quest_item)
rarity: Rarity tier (common, uncommon, rare, epic, legendary)
description: Item lore and information
value: Gold value for buying/selling
is_tradeable: Whether item can be sold on marketplace
@@ -49,7 +50,8 @@ class Item:
item_id: str
name: str
item_type: ItemType
description: str
rarity: ItemRarity = ItemRarity.COMMON
description: str = ""
value: int = 0
is_tradeable: bool = True
@@ -158,6 +160,7 @@ class Item:
"""
data = asdict(self)
data["item_type"] = self.item_type.value
data["rarity"] = self.rarity.value
if self.damage_type:
data["damage_type"] = self.damage_type.value
if self.elemental_damage_type:
@@ -178,6 +181,7 @@ class Item:
"""
# Convert string values back to enums
item_type = ItemType(data["item_type"])
rarity = ItemRarity(data.get("rarity", "common"))
damage_type = DamageType(data["damage_type"]) if data.get("damage_type") else None
elemental_damage_type = (
DamageType(data["elemental_damage_type"])
@@ -194,7 +198,8 @@ class Item:
item_id=data["item_id"],
name=data["name"],
item_type=item_type,
description=data["description"],
rarity=rarity,
description=data.get("description", ""),
value=data.get("value", 0),
is_tradeable=data.get("is_tradeable", True),
stat_bonuses=data.get("stat_bonuses", {}),

387
api/tests/test_items.py Normal file
View File

@@ -0,0 +1,387 @@
"""
Unit tests for Item dataclass and ItemRarity enum.
Tests item creation, rarity, type checking, and serialization.
"""
import pytest
from app.models.items import Item
from app.models.enums import ItemType, ItemRarity, DamageType
class TestItemRarityEnum:
"""Tests for ItemRarity enum."""
def test_rarity_values(self):
"""Test all rarity values exist and have correct string values."""
assert ItemRarity.COMMON.value == "common"
assert ItemRarity.UNCOMMON.value == "uncommon"
assert ItemRarity.RARE.value == "rare"
assert ItemRarity.EPIC.value == "epic"
assert ItemRarity.LEGENDARY.value == "legendary"
def test_rarity_from_string(self):
"""Test creating rarity from string value."""
assert ItemRarity("common") == ItemRarity.COMMON
assert ItemRarity("uncommon") == ItemRarity.UNCOMMON
assert ItemRarity("rare") == ItemRarity.RARE
assert ItemRarity("epic") == ItemRarity.EPIC
assert ItemRarity("legendary") == ItemRarity.LEGENDARY
def test_rarity_count(self):
"""Test that there are exactly 5 rarity tiers."""
assert len(ItemRarity) == 5
class TestItemCreation:
"""Tests for creating Item instances."""
def test_create_basic_item(self):
"""Test creating a basic item with minimal fields."""
item = Item(
item_id="test_item",
name="Test Item",
item_type=ItemType.QUEST_ITEM,
)
assert item.item_id == "test_item"
assert item.name == "Test Item"
assert item.item_type == ItemType.QUEST_ITEM
assert item.rarity == ItemRarity.COMMON # Default
assert item.description == ""
assert item.value == 0
assert item.is_tradeable == True
def test_item_default_rarity_is_common(self):
"""Test that items default to COMMON rarity."""
item = Item(
item_id="sword_1",
name="Iron Sword",
item_type=ItemType.WEAPON,
)
assert item.rarity == ItemRarity.COMMON
def test_create_item_with_rarity(self):
"""Test creating items with different rarity levels."""
uncommon = Item(
item_id="sword_2",
name="Steel Sword",
item_type=ItemType.WEAPON,
rarity=ItemRarity.UNCOMMON,
)
assert uncommon.rarity == ItemRarity.UNCOMMON
rare = Item(
item_id="sword_3",
name="Mithril Sword",
item_type=ItemType.WEAPON,
rarity=ItemRarity.RARE,
)
assert rare.rarity == ItemRarity.RARE
epic = Item(
item_id="sword_4",
name="Dragon Sword",
item_type=ItemType.WEAPON,
rarity=ItemRarity.EPIC,
)
assert epic.rarity == ItemRarity.EPIC
legendary = Item(
item_id="sword_5",
name="Excalibur",
item_type=ItemType.WEAPON,
rarity=ItemRarity.LEGENDARY,
)
assert legendary.rarity == ItemRarity.LEGENDARY
def test_create_weapon(self):
"""Test creating a weapon with all weapon-specific fields."""
weapon = Item(
item_id="fire_sword",
name="Flame Blade",
item_type=ItemType.WEAPON,
rarity=ItemRarity.RARE,
description="A sword wreathed in flames.",
value=500,
damage=25,
damage_type=DamageType.PHYSICAL,
crit_chance=0.15,
crit_multiplier=2.5,
elemental_damage_type=DamageType.FIRE,
physical_ratio=0.7,
elemental_ratio=0.3,
)
assert weapon.is_weapon() == True
assert weapon.is_elemental_weapon() == True
assert weapon.damage == 25
assert weapon.crit_chance == 0.15
assert weapon.crit_multiplier == 2.5
assert weapon.elemental_damage_type == DamageType.FIRE
assert weapon.physical_ratio == 0.7
assert weapon.elemental_ratio == 0.3
def test_create_armor(self):
"""Test creating armor with defense/resistance."""
armor = Item(
item_id="plate_armor",
name="Steel Plate Armor",
item_type=ItemType.ARMOR,
rarity=ItemRarity.UNCOMMON,
description="Heavy steel armor.",
value=300,
defense=15,
resistance=5,
)
assert armor.is_armor() == True
assert armor.defense == 15
assert armor.resistance == 5
def test_create_consumable(self):
"""Test creating a consumable item."""
potion = Item(
item_id="health_potion",
name="Health Potion",
item_type=ItemType.CONSUMABLE,
rarity=ItemRarity.COMMON,
description="Restores 50 HP.",
value=25,
)
assert potion.is_consumable() == True
assert potion.is_tradeable == True
class TestItemTypeMethods:
"""Tests for item type checking methods."""
def test_is_weapon(self):
"""Test is_weapon() method."""
weapon = Item(item_id="w", name="W", item_type=ItemType.WEAPON)
armor = Item(item_id="a", name="A", item_type=ItemType.ARMOR)
assert weapon.is_weapon() == True
assert armor.is_weapon() == False
def test_is_armor(self):
"""Test is_armor() method."""
weapon = Item(item_id="w", name="W", item_type=ItemType.WEAPON)
armor = Item(item_id="a", name="A", item_type=ItemType.ARMOR)
assert armor.is_armor() == True
assert weapon.is_armor() == False
def test_is_consumable(self):
"""Test is_consumable() method."""
consumable = Item(item_id="c", name="C", item_type=ItemType.CONSUMABLE)
weapon = Item(item_id="w", name="W", item_type=ItemType.WEAPON)
assert consumable.is_consumable() == True
assert weapon.is_consumable() == False
def test_is_quest_item(self):
"""Test is_quest_item() method."""
quest = Item(item_id="q", name="Q", item_type=ItemType.QUEST_ITEM)
weapon = Item(item_id="w", name="W", item_type=ItemType.WEAPON)
assert quest.is_quest_item() == True
assert weapon.is_quest_item() == False
class TestItemSerialization:
"""Tests for Item serialization and deserialization."""
def test_to_dict_includes_rarity(self):
"""Test that to_dict() includes rarity as string."""
item = Item(
item_id="test",
name="Test",
item_type=ItemType.WEAPON,
rarity=ItemRarity.EPIC,
description="Test item",
)
data = item.to_dict()
assert data["rarity"] == "epic"
assert data["item_type"] == "weapon"
def test_from_dict_parses_rarity(self):
"""Test that from_dict() parses rarity correctly."""
data = {
"item_id": "test",
"name": "Test",
"item_type": "weapon",
"rarity": "legendary",
"description": "Test item",
}
item = Item.from_dict(data)
assert item.rarity == ItemRarity.LEGENDARY
assert item.item_type == ItemType.WEAPON
def test_from_dict_defaults_to_common_rarity(self):
"""Test that from_dict() defaults to COMMON if rarity missing."""
data = {
"item_id": "test",
"name": "Test",
"item_type": "weapon",
"description": "Test item",
# No rarity field
}
item = Item.from_dict(data)
assert item.rarity == ItemRarity.COMMON
def test_round_trip_serialization(self):
"""Test serialization and deserialization preserve all data."""
original = Item(
item_id="flame_sword",
name="Flame Blade",
item_type=ItemType.WEAPON,
rarity=ItemRarity.RARE,
description="A fiery blade.",
value=500,
damage=25,
damage_type=DamageType.PHYSICAL,
crit_chance=0.12,
crit_multiplier=2.5,
elemental_damage_type=DamageType.FIRE,
physical_ratio=0.7,
elemental_ratio=0.3,
defense=0,
resistance=0,
required_level=5,
stat_bonuses={"strength": 3},
)
# Serialize then deserialize
data = original.to_dict()
restored = Item.from_dict(data)
assert restored.item_id == original.item_id
assert restored.name == original.name
assert restored.item_type == original.item_type
assert restored.rarity == original.rarity
assert restored.description == original.description
assert restored.value == original.value
assert restored.damage == original.damage
assert restored.damage_type == original.damage_type
assert restored.crit_chance == original.crit_chance
assert restored.crit_multiplier == original.crit_multiplier
assert restored.elemental_damage_type == original.elemental_damage_type
assert restored.physical_ratio == original.physical_ratio
assert restored.elemental_ratio == original.elemental_ratio
assert restored.required_level == original.required_level
assert restored.stat_bonuses == original.stat_bonuses
def test_round_trip_all_rarities(self):
"""Test round-trip serialization for all rarity levels."""
for rarity in ItemRarity:
original = Item(
item_id=f"item_{rarity.value}",
name=f"{rarity.value.title()} Item",
item_type=ItemType.CONSUMABLE,
rarity=rarity,
)
data = original.to_dict()
restored = Item.from_dict(data)
assert restored.rarity == rarity
class TestItemEquippability:
"""Tests for item equip requirements."""
def test_can_equip_level_requirement(self):
"""Test level requirement checking."""
item = Item(
item_id="high_level_sword",
name="Epic Sword",
item_type=ItemType.WEAPON,
required_level=10,
)
assert item.can_equip(character_level=5) == False
assert item.can_equip(character_level=10) == True
assert item.can_equip(character_level=15) == True
def test_can_equip_class_requirement(self):
"""Test class requirement checking."""
item = Item(
item_id="mage_staff",
name="Mage Staff",
item_type=ItemType.WEAPON,
required_class="mage",
)
assert item.can_equip(character_level=1, character_class="warrior") == False
assert item.can_equip(character_level=1, character_class="mage") == True
class TestItemStatBonuses:
"""Tests for item stat bonus methods."""
def test_get_total_stat_bonus(self):
"""Test getting stat bonuses from items."""
item = Item(
item_id="ring_of_power",
name="Ring of Power",
item_type=ItemType.ARMOR,
stat_bonuses={"strength": 5, "constitution": 3},
)
assert item.get_total_stat_bonus("strength") == 5
assert item.get_total_stat_bonus("constitution") == 3
assert item.get_total_stat_bonus("dexterity") == 0 # Not in bonuses
class TestItemRepr:
"""Tests for item string representation."""
def test_weapon_repr(self):
"""Test weapon __repr__ output."""
weapon = Item(
item_id="sword",
name="Iron Sword",
item_type=ItemType.WEAPON,
damage=10,
value=50,
)
repr_str = repr(weapon)
assert "Iron Sword" in repr_str
assert "weapon" in repr_str
def test_armor_repr(self):
"""Test armor __repr__ output."""
armor = Item(
item_id="armor",
name="Leather Armor",
item_type=ItemType.ARMOR,
defense=5,
value=30,
)
repr_str = repr(armor)
assert "Leather Armor" in repr_str
assert "armor" in repr_str
def test_consumable_repr(self):
"""Test consumable __repr__ output."""
potion = Item(
item_id="potion",
name="Health Potion",
item_type=ItemType.CONSUMABLE,
value=10,
)
repr_str = repr(potion)
assert "Health Potion" in repr_str
assert "consumable" in repr_str