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)
This commit is contained in:
2025-11-27 00:01:17 -06:00
parent a38906b445
commit fdd48034e4
14 changed files with 2257 additions and 26 deletions

View File

@@ -21,35 +21,92 @@ class EnemyDifficulty(Enum):
BOSS = "boss"
class LootType(Enum):
"""
Types of loot drops in enemy loot tables.
STATIC: Fixed item_id reference (consumables, quest items, materials)
PROCEDURAL: Procedurally generated equipment (weapons, armor with affixes)
"""
STATIC = "static"
PROCEDURAL = "procedural"
@dataclass
class LootEntry:
"""
Single entry in an enemy's loot table.
Supports two types of loot:
STATIC loot (default):
- item_id references a predefined item (health_potion, gold_coin, etc.)
- quantity_min/max define stack size
PROCEDURAL loot:
- item_type specifies "weapon" or "armor"
- rarity_bonus adds to rarity roll (difficulty contribution)
- Generated equipment uses the ItemGenerator system
Attributes:
item_id: Reference to item definition
loot_type: Type of loot (static or procedural)
drop_chance: Probability of dropping (0.0 to 1.0)
quantity_min: Minimum quantity if dropped
quantity_max: Maximum quantity if dropped
item_id: Reference to item definition (for STATIC loot)
item_type: Type of equipment to generate (for PROCEDURAL loot)
rarity_bonus: Added to rarity roll (0.0 to 0.5, for PROCEDURAL)
"""
item_id: str
# Common fields
loot_type: LootType = LootType.STATIC
drop_chance: float = 0.1
quantity_min: int = 1
quantity_max: int = 1
# Static loot fields
item_id: Optional[str] = None
# Procedural loot fields
item_type: Optional[str] = None # "weapon" or "armor"
rarity_bonus: float = 0.0 # Added to rarity roll (0.0 to 0.5)
def to_dict(self) -> Dict[str, Any]:
"""Serialize loot entry to dictionary."""
return asdict(self)
data = {
"loot_type": self.loot_type.value,
"drop_chance": self.drop_chance,
"quantity_min": self.quantity_min,
"quantity_max": self.quantity_max,
}
# Only include relevant fields based on loot type
if self.item_id is not None:
data["item_id"] = self.item_id
if self.item_type is not None:
data["item_type"] = self.item_type
data["rarity_bonus"] = self.rarity_bonus
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'LootEntry':
"""Deserialize loot entry from dictionary."""
"""
Deserialize loot entry from dictionary.
Backward compatible: entries without loot_type default to STATIC,
and item_id is required for STATIC entries (for backward compat).
"""
# Parse loot type with backward compatibility
loot_type_str = data.get("loot_type", "static")
loot_type = LootType(loot_type_str)
return cls(
item_id=data["item_id"],
loot_type=loot_type,
drop_chance=data.get("drop_chance", 0.1),
quantity_min=data.get("quantity_min", 1),
quantity_max=data.get("quantity_max", 1),
item_id=data.get("item_id"),
item_type=data.get("item_type"),
rarity_bonus=data.get("rarity_bonus", 0.0),
)