Files
Code_of_Conquest/api/tests/test_static_item_loader.py
Phillip Tarrant fdd48034e4 feat(api): implement combat loot integration with hybrid static/procedural system
Add CombatLootService that orchestrates loot generation from combat,
supporting both static item drops (consumables, materials) and procedural
equipment generation (weapons, armor with affixes).

Key changes:
- Extend LootEntry model with LootType enum (STATIC/PROCEDURAL)
- Create StaticItemLoader service for consumables/materials from YAML
- Create CombatLootService with full rarity formula incorporating:
  - Party average level
  - Enemy difficulty tier (EASY: +0%, MEDIUM: +5%, HARD: +15%, BOSS: +30%)
  - Character luck stat
  - Per-entry rarity bonus
- Integrate with CombatService._calculate_rewards() for automatic loot gen
- Add boss guaranteed drops via generate_boss_loot()

New enemy variants (goblin family proof-of-concept):
- goblin_scout (Easy) - static drops only
- goblin_warrior (Medium) - static + procedural weapon drops
- goblin_chieftain (Hard) - static + procedural weapon/armor drops

Static items added:
- consumables.yaml: health/mana potions, elixirs, food
- materials.yaml: trophy items, crafting materials

Tests: 59 new tests across 3 test files (all passing)
2025-11-27 00:01:17 -06:00

195 lines
6.3 KiB
Python

"""
Tests for StaticItemLoader service.
Tests the service that loads predefined items (consumables, materials)
from YAML files for use in loot tables.
"""
import pytest
from pathlib import Path
from app.services.static_item_loader import StaticItemLoader, get_static_item_loader
from app.models.enums import ItemType, ItemRarity
class TestStaticItemLoaderInitialization:
"""Test service initialization."""
def test_init_with_default_path(self):
"""Service should initialize with default data path."""
loader = StaticItemLoader()
assert loader.data_dir.exists() or not loader._loaded
def test_init_with_custom_path(self, tmp_path):
"""Service should accept custom data path."""
loader = StaticItemLoader(data_dir=str(tmp_path))
assert loader.data_dir == tmp_path
def test_singleton_returns_same_instance(self):
"""get_static_item_loader should return singleton."""
loader1 = get_static_item_loader()
loader2 = get_static_item_loader()
assert loader1 is loader2
class TestStaticItemLoaderLoading:
"""Test YAML loading functionality."""
def test_loads_consumables(self):
"""Should load consumable items from YAML."""
loader = get_static_item_loader()
# Check that health potion exists
assert loader.has_item("health_potion_small")
assert loader.has_item("health_potion_medium")
def test_loads_materials(self):
"""Should load material items from YAML."""
loader = get_static_item_loader()
# Check that materials exist
assert loader.has_item("goblin_ear")
assert loader.has_item("wolf_pelt")
def test_get_all_item_ids_returns_list(self):
"""get_all_item_ids should return list of item IDs."""
loader = get_static_item_loader()
item_ids = loader.get_all_item_ids()
assert isinstance(item_ids, list)
assert len(item_ids) > 0
assert "health_potion_small" in item_ids
def test_has_item_returns_false_for_missing(self):
"""has_item should return False for non-existent items."""
loader = get_static_item_loader()
assert not loader.has_item("nonexistent_item_xyz")
class TestStaticItemLoaderGetItem:
"""Test item retrieval."""
def test_get_item_returns_item_object(self):
"""get_item should return an Item instance."""
loader = get_static_item_loader()
item = loader.get_item("health_potion_small")
assert item is not None
assert item.name == "Small Health Potion"
assert item.item_type == ItemType.CONSUMABLE
assert item.rarity == ItemRarity.COMMON
def test_get_item_has_unique_id(self):
"""Each call should create item with unique ID."""
loader = get_static_item_loader()
item1 = loader.get_item("health_potion_small")
item2 = loader.get_item("health_potion_small")
assert item1.item_id != item2.item_id
assert "health_potion_small" in item1.item_id
assert "health_potion_small" in item2.item_id
def test_get_item_returns_none_for_missing(self):
"""get_item should return None for non-existent items."""
loader = get_static_item_loader()
item = loader.get_item("nonexistent_item_xyz")
assert item is None
def test_get_item_consumable_has_effects(self):
"""Consumable items should have effects_on_use."""
loader = get_static_item_loader()
item = loader.get_item("health_potion_small")
assert len(item.effects_on_use) > 0
effect = item.effects_on_use[0]
assert effect.name == "Minor Healing"
assert effect.power > 0
def test_get_item_quest_item_type(self):
"""Quest items should have correct type."""
loader = get_static_item_loader()
item = loader.get_item("goblin_ear")
assert item is not None
assert item.item_type == ItemType.QUEST_ITEM
assert item.rarity == ItemRarity.COMMON
def test_get_item_has_value(self):
"""Items should have value set."""
loader = get_static_item_loader()
item = loader.get_item("health_potion_small")
assert item.value > 0
def test_get_item_is_tradeable(self):
"""Items should default to tradeable."""
loader = get_static_item_loader()
item = loader.get_item("goblin_ear")
assert item.is_tradeable is True
class TestStaticItemLoaderVariousItems:
"""Test loading various item types."""
def test_medium_health_potion(self):
"""Test medium health potion properties."""
loader = get_static_item_loader()
item = loader.get_item("health_potion_medium")
assert item is not None
assert item.rarity == ItemRarity.UNCOMMON
assert item.value > 25 # More expensive than small
def test_large_health_potion(self):
"""Test large health potion properties."""
loader = get_static_item_loader()
item = loader.get_item("health_potion_large")
assert item is not None
assert item.rarity == ItemRarity.RARE
def test_chieftain_token_rarity(self):
"""Test that chieftain token is rare."""
loader = get_static_item_loader()
item = loader.get_item("goblin_chieftain_token")
assert item is not None
assert item.rarity == ItemRarity.RARE
def test_elixir_has_buff_effect(self):
"""Test that elixirs have buff effects."""
loader = get_static_item_loader()
item = loader.get_item("elixir_of_strength")
if item: # Only test if item exists
assert len(item.effects_on_use) > 0
class TestStaticItemLoaderCache:
"""Test caching behavior."""
def test_clear_cache(self):
"""clear_cache should reset loaded state."""
loader = StaticItemLoader()
# Trigger loading
loader._ensure_loaded()
assert loader._loaded is True
# Clear cache
loader.clear_cache()
assert loader._loaded is False
assert len(loader._cache) == 0
def test_lazy_loading(self):
"""Items should be loaded lazily on first access."""
loader = StaticItemLoader()
assert loader._loaded is False
# Access triggers loading
_ = loader.has_item("health_potion_small")
assert loader._loaded is True