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)
195 lines
6.3 KiB
Python
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
|