feat/phase4-combat-foundation #8
@@ -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",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
387
api/tests/test_items.py
Normal 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
|
||||
Reference in New Issue
Block a user