Combat foundation complete

This commit is contained in:
2025-11-27 22:18:58 -06:00
parent dd92cf5991
commit 6d3fb63355
33 changed files with 1870 additions and 85 deletions

View File

@@ -20,7 +20,7 @@ from app.models.stats import Stats
from app.models.abilities import Ability, AbilityLoader
from app.models.effects import Effect
from app.models.items import Item
from app.models.enums import CombatStatus, AbilityType, DamageType, EffectType
from app.models.enums import CombatStatus, AbilityType, DamageType, EffectType, StatType
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
@@ -94,6 +94,7 @@ class ActionResult:
combat_status: Final combat status if ended
next_combatant_id: ID of combatant whose turn is next
turn_effects: Effects that triggered at turn start/end
rewards: Combat rewards if victory (XP, gold, items)
"""
success: bool
@@ -105,6 +106,7 @@ class ActionResult:
next_combatant_id: Optional[str] = None
next_is_player: bool = True # True if next turn is player's
turn_effects: List[Dict[str, Any]] = field(default_factory=list)
rewards: Optional[Dict[str, Any]] = None # Populated on victory
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for API response."""
@@ -127,6 +129,7 @@ class ActionResult:
"next_combatant_id": self.next_combatant_id,
"next_is_player": self.next_is_player,
"turn_effects": self.turn_effects,
"rewards": self.rewards,
}
@@ -451,6 +454,113 @@ class CombatService:
return rewards
def check_existing_combat(
self,
session_id: str,
user_id: str
) -> Optional[Dict[str, Any]]:
"""
Check if a session has an existing active combat encounter.
Returns combat summary if exists, None otherwise.
Args:
session_id: Game session ID
user_id: User ID for authorization
Returns:
Dictionary with combat summary if in combat, None otherwise
"""
logger.info("Checking for existing combat",
session_id=session_id)
session = self.session_service.get_session(session_id, user_id)
if not session.is_in_combat():
return None
# Get encounter details
encounter = self.get_combat_state(session_id, user_id)
if not encounter:
return None
# Build summary of combatants
players = []
enemies = []
for combatant in encounter.combatants:
combatant_info = {
"name": combatant.name,
"current_hp": combatant.current_hp,
"max_hp": combatant.max_hp,
"is_alive": combatant.is_alive(),
}
if combatant.is_player:
players.append(combatant_info)
else:
enemies.append(combatant_info)
return {
"has_active_combat": True,
"encounter_id": encounter.encounter_id,
"round_number": encounter.round_number,
"status": encounter.status.value,
"players": players,
"enemies": enemies,
}
def abandon_combat(
self,
session_id: str,
user_id: str
) -> bool:
"""
Abandon an existing combat encounter without completing it.
Deletes the encounter from the database and clears the session
reference. No rewards are distributed.
Args:
session_id: Game session ID
user_id: User ID for authorization
Returns:
True if combat was abandoned, False if no combat existed
"""
logger.info("Abandoning combat",
session_id=session_id)
session = self.session_service.get_session(session_id, user_id)
if not session.is_in_combat():
logger.info("No combat to abandon",
session_id=session_id)
return False
encounter_id = session.active_combat_encounter_id
# Delete encounter from repository
if encounter_id:
try:
self.combat_repository.delete_encounter(encounter_id)
logger.info("Deleted encounter from repository",
encounter_id=encounter_id)
except Exception as e:
logger.warning("Failed to delete encounter from repository",
encounter_id=encounter_id,
error=str(e))
# Clear session combat references
session.active_combat_encounter_id = None
session.combat_encounter = None # Clear legacy field too
session.update_activity()
self.session_service.update_session(session)
logger.info("Combat abandoned",
session_id=session_id,
encounter_id=encounter_id)
return True
# =========================================================================
# Action Execution
# =========================================================================
@@ -549,6 +659,7 @@ class CombatService:
if status == CombatStatus.VICTORY:
rewards = self._calculate_rewards(encounter, session, user_id)
result.message += f" Victory! Earned {rewards.experience} XP and {rewards.gold} gold."
result.rewards = rewards.to_dict()
# End encounter in repository
if session.active_combat_encounter_id:
@@ -699,6 +810,11 @@ class CombatService:
result.combat_ended = True
result.combat_status = status
# Calculate and distribute rewards on victory
if status == CombatStatus.VICTORY:
rewards = self._calculate_rewards(encounter, session, user_id)
result.rewards = rewards.to_dict()
# End encounter in repository
if session.active_combat_encounter_id:
self.combat_repository.end_encounter(
@@ -946,7 +1062,7 @@ class CombatService:
effect_type=EffectType.BUFF,
duration=1,
power=5, # +5 defense
stat_affected="constitution",
stat_affected=StatType.CONSTITUTION,
source="defend_action",
)
combatant.add_effect(defense_buff)