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:
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user