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:
@@ -15,7 +15,7 @@ from uuid import uuid4
|
||||
|
||||
from app.models.combat import Combatant, CombatEncounter
|
||||
from app.models.character import Character
|
||||
from app.models.enemy import EnemyTemplate
|
||||
from app.models.enemy import EnemyTemplate, EnemyDifficulty
|
||||
from app.models.stats import Stats
|
||||
from app.models.abilities import Ability, AbilityLoader
|
||||
from app.models.effects import Effect
|
||||
@@ -25,6 +25,11 @@ from app.services.damage_calculator import DamageCalculator, DamageResult
|
||||
from app.services.enemy_loader import EnemyLoader, get_enemy_loader
|
||||
from app.services.session_service import get_session_service
|
||||
from app.services.character_service import get_character_service
|
||||
from app.services.combat_loot_service import (
|
||||
get_combat_loot_service,
|
||||
CombatLootService,
|
||||
LootContext
|
||||
)
|
||||
from app.utils.logging import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -197,6 +202,7 @@ class CombatService:
|
||||
self.character_service = get_character_service()
|
||||
self.enemy_loader = get_enemy_loader()
|
||||
self.ability_loader = AbilityLoader()
|
||||
self.loot_service = get_combat_loot_service()
|
||||
|
||||
logger.info("CombatService initialized")
|
||||
|
||||
@@ -898,6 +904,9 @@ class CombatService:
|
||||
"""
|
||||
Calculate and distribute rewards after victory.
|
||||
|
||||
Uses CombatLootService for loot generation, supporting both
|
||||
static items (consumables) and procedural equipment.
|
||||
|
||||
Args:
|
||||
encounter: Completed combat encounter
|
||||
session: Game session
|
||||
@@ -908,6 +917,9 @@ class CombatService:
|
||||
"""
|
||||
rewards = CombatRewards()
|
||||
|
||||
# Build loot context from encounter
|
||||
loot_context = self._build_loot_context(encounter)
|
||||
|
||||
# Sum up rewards from defeated enemies
|
||||
for combatant in encounter.combatants:
|
||||
if not combatant.is_player and combatant.is_dead():
|
||||
@@ -919,9 +931,28 @@ class CombatService:
|
||||
rewards.experience += enemy.experience_reward
|
||||
rewards.gold += enemy.get_gold_reward()
|
||||
|
||||
# Roll for loot
|
||||
loot = enemy.roll_loot()
|
||||
rewards.items.extend(loot)
|
||||
# Generate loot using the loot service
|
||||
# Update context with this specific enemy's difficulty
|
||||
enemy_context = LootContext(
|
||||
party_average_level=loot_context.party_average_level,
|
||||
enemy_difficulty=enemy.difficulty,
|
||||
luck_stat=loot_context.luck_stat,
|
||||
loot_bonus=loot_context.loot_bonus
|
||||
)
|
||||
|
||||
# Use boss loot for boss enemies
|
||||
if enemy.is_boss():
|
||||
loot_items = self.loot_service.generate_boss_loot(
|
||||
enemy, enemy_context
|
||||
)
|
||||
else:
|
||||
loot_items = self.loot_service.generate_loot_from_enemy(
|
||||
enemy, enemy_context
|
||||
)
|
||||
|
||||
# Convert Item objects to dicts for serialization
|
||||
for item in loot_items:
|
||||
rewards.items.append(item.to_dict())
|
||||
|
||||
# Distribute rewards to player characters
|
||||
player_combatants = [c for c in encounter.combatants if c.is_player]
|
||||
@@ -964,6 +995,49 @@ class CombatService:
|
||||
|
||||
return rewards
|
||||
|
||||
def _build_loot_context(self, encounter: CombatEncounter) -> LootContext:
|
||||
"""
|
||||
Build loot generation context from a combat encounter.
|
||||
|
||||
Calculates:
|
||||
- Party average level
|
||||
- Party average luck stat
|
||||
- Default difficulty (uses EASY, specific enemies override)
|
||||
|
||||
Args:
|
||||
encounter: Combat encounter with player combatants
|
||||
|
||||
Returns:
|
||||
LootContext for loot generation
|
||||
"""
|
||||
player_combatants = [c for c in encounter.combatants if c.is_player]
|
||||
|
||||
# Calculate party average level
|
||||
if player_combatants:
|
||||
# Use combatant's level if available, otherwise default to 1
|
||||
levels = []
|
||||
for p in player_combatants:
|
||||
# Try to get level from stats or combatant
|
||||
level = getattr(p, 'level', 1)
|
||||
levels.append(level)
|
||||
avg_level = sum(levels) // len(levels) if levels else 1
|
||||
else:
|
||||
avg_level = 1
|
||||
|
||||
# Calculate party average luck
|
||||
if player_combatants:
|
||||
luck_values = [p.stats.luck for p in player_combatants]
|
||||
avg_luck = sum(luck_values) // len(luck_values) if luck_values else 8
|
||||
else:
|
||||
avg_luck = 8
|
||||
|
||||
return LootContext(
|
||||
party_average_level=avg_level,
|
||||
enemy_difficulty=EnemyDifficulty.EASY, # Default; overridden per-enemy
|
||||
luck_stat=avg_luck,
|
||||
loot_bonus=0.0 # Future: add buffs/abilities bonus
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Helper Methods
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user