combat testing and polishing in the dev console, many bug fixes
This commit is contained in:
@@ -30,6 +30,10 @@ from app.services.combat_loot_service import (
|
||||
CombatLootService,
|
||||
LootContext
|
||||
)
|
||||
from app.services.combat_repository import (
|
||||
get_combat_repository,
|
||||
CombatRepository
|
||||
)
|
||||
from app.utils.logging import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -99,6 +103,7 @@ class ActionResult:
|
||||
combat_ended: bool = False
|
||||
combat_status: Optional[CombatStatus] = None
|
||||
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)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -120,6 +125,7 @@ class ActionResult:
|
||||
"combat_ended": self.combat_ended,
|
||||
"combat_status": self.combat_status.value if self.combat_status else None,
|
||||
"next_combatant_id": self.next_combatant_id,
|
||||
"next_is_player": self.next_is_player,
|
||||
"turn_effects": self.turn_effects,
|
||||
}
|
||||
|
||||
@@ -203,6 +209,7 @@ class CombatService:
|
||||
self.enemy_loader = get_enemy_loader()
|
||||
self.ability_loader = AbilityLoader()
|
||||
self.loot_service = get_combat_loot_service()
|
||||
self.combat_repository = get_combat_repository()
|
||||
|
||||
logger.info("CombatService initialized")
|
||||
|
||||
@@ -283,9 +290,18 @@ class CombatService:
|
||||
# Initialize combat (roll initiative, set turn order)
|
||||
encounter.initialize_combat()
|
||||
|
||||
# Store in session
|
||||
session.start_combat(encounter)
|
||||
self.session_service.update_session(session, user_id)
|
||||
# Save encounter to dedicated table
|
||||
self.combat_repository.create_encounter(
|
||||
encounter=encounter,
|
||||
session_id=session_id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Update session with reference to encounter (not inline data)
|
||||
session.active_combat_encounter_id = encounter.encounter_id
|
||||
session.combat_encounter = None # Clear legacy inline storage
|
||||
session.update_activity()
|
||||
self.session_service.update_session(session)
|
||||
|
||||
logger.info("Combat started",
|
||||
session_id=session_id,
|
||||
@@ -303,6 +319,9 @@ class CombatService:
|
||||
"""
|
||||
Get current combat state for a session.
|
||||
|
||||
Uses the new database-backed storage, with fallback to legacy
|
||||
inline session storage for backward compatibility.
|
||||
|
||||
Args:
|
||||
session_id: Game session ID
|
||||
user_id: User ID for authorization
|
||||
@@ -311,7 +330,66 @@ class CombatService:
|
||||
CombatEncounter if in combat, None otherwise
|
||||
"""
|
||||
session = self.session_service.get_session(session_id, user_id)
|
||||
return session.combat_encounter
|
||||
|
||||
# New system: Check for reference to combat_encounters table
|
||||
if session.active_combat_encounter_id:
|
||||
encounter = self.combat_repository.get_encounter(
|
||||
session.active_combat_encounter_id
|
||||
)
|
||||
if encounter:
|
||||
return encounter
|
||||
# Reference exists but encounter not found - clear stale reference
|
||||
logger.warning("Stale combat encounter reference, clearing",
|
||||
session_id=session_id,
|
||||
encounter_id=session.active_combat_encounter_id)
|
||||
session.active_combat_encounter_id = None
|
||||
self.session_service.update_session(session)
|
||||
return None
|
||||
|
||||
# Legacy fallback: Check inline combat data and migrate if present
|
||||
if session.combat_encounter:
|
||||
return self._migrate_inline_encounter(session, user_id)
|
||||
|
||||
return None
|
||||
|
||||
def _migrate_inline_encounter(
|
||||
self,
|
||||
session,
|
||||
user_id: str
|
||||
) -> CombatEncounter:
|
||||
"""
|
||||
Migrate legacy inline combat encounter to database table.
|
||||
|
||||
This provides backward compatibility by automatically migrating
|
||||
existing inline combat data to the new database-backed system
|
||||
on first access.
|
||||
|
||||
Args:
|
||||
session: GameSession with inline combat_encounter
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
The migrated CombatEncounter
|
||||
"""
|
||||
encounter = session.combat_encounter
|
||||
|
||||
logger.info("Migrating inline combat encounter to database",
|
||||
session_id=session.session_id,
|
||||
encounter_id=encounter.encounter_id)
|
||||
|
||||
# Save to repository
|
||||
self.combat_repository.create_encounter(
|
||||
encounter=encounter,
|
||||
session_id=session.session_id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Update session to use reference
|
||||
session.active_combat_encounter_id = encounter.encounter_id
|
||||
session.combat_encounter = None # Clear inline data
|
||||
self.session_service.update_session(session)
|
||||
|
||||
return encounter
|
||||
|
||||
def end_combat(
|
||||
self,
|
||||
@@ -339,7 +417,11 @@ class CombatService:
|
||||
if not session.is_in_combat():
|
||||
raise NotInCombatError("Session is not in combat")
|
||||
|
||||
encounter = session.combat_encounter
|
||||
# Get encounter from repository (or legacy inline)
|
||||
encounter = self.get_combat_state(session_id, user_id)
|
||||
if not encounter:
|
||||
raise NotInCombatError("Combat encounter not found")
|
||||
|
||||
encounter.status = outcome
|
||||
|
||||
# Calculate rewards if victory
|
||||
@@ -347,12 +429,22 @@ class CombatService:
|
||||
if outcome == CombatStatus.VICTORY:
|
||||
rewards = self._calculate_rewards(encounter, session, user_id)
|
||||
|
||||
# End combat on session
|
||||
session.end_combat()
|
||||
self.session_service.update_session(session, user_id)
|
||||
# End encounter in repository
|
||||
if session.active_combat_encounter_id:
|
||||
self.combat_repository.end_encounter(
|
||||
encounter_id=session.active_combat_encounter_id,
|
||||
status=outcome
|
||||
)
|
||||
|
||||
# Clear session combat reference
|
||||
session.active_combat_encounter_id = None
|
||||
session.combat_encounter = None # Also clear legacy field
|
||||
session.update_activity()
|
||||
self.session_service.update_session(session)
|
||||
|
||||
logger.info("Combat ended",
|
||||
session_id=session_id,
|
||||
encounter_id=encounter.encounter_id,
|
||||
outcome=outcome.value,
|
||||
xp_earned=rewards.experience,
|
||||
gold_earned=rewards.gold)
|
||||
@@ -396,7 +488,10 @@ class CombatService:
|
||||
if not session.is_in_combat():
|
||||
raise NotInCombatError("Session is not in combat")
|
||||
|
||||
encounter = session.combat_encounter
|
||||
# Get encounter from repository
|
||||
encounter = self.get_combat_state(session_id, user_id)
|
||||
if not encounter:
|
||||
raise NotInCombatError("Combat encounter not found")
|
||||
|
||||
# Validate it's this combatant's turn
|
||||
current = encounter.get_current_combatant()
|
||||
@@ -455,15 +550,29 @@ class CombatService:
|
||||
rewards = self._calculate_rewards(encounter, session, user_id)
|
||||
result.message += f" Victory! Earned {rewards.experience} XP and {rewards.gold} gold."
|
||||
|
||||
session.end_combat()
|
||||
# End encounter in repository
|
||||
if session.active_combat_encounter_id:
|
||||
self.combat_repository.end_encounter(
|
||||
encounter_id=session.active_combat_encounter_id,
|
||||
status=status
|
||||
)
|
||||
|
||||
# Clear session combat reference
|
||||
session.active_combat_encounter_id = None
|
||||
session.combat_encounter = None
|
||||
else:
|
||||
# Advance turn
|
||||
# Advance turn and save to repository
|
||||
self._advance_turn_and_save(encounter, session, user_id)
|
||||
next_combatant = encounter.get_current_combatant()
|
||||
result.next_combatant_id = next_combatant.combatant_id if next_combatant else None
|
||||
if next_combatant:
|
||||
result.next_combatant_id = next_combatant.combatant_id
|
||||
result.next_is_player = next_combatant.is_player
|
||||
else:
|
||||
result.next_combatant_id = None
|
||||
result.next_is_player = True
|
||||
|
||||
# Save session state
|
||||
self.session_service.update_session(session, user_id)
|
||||
self.session_service.update_session(session)
|
||||
|
||||
return result
|
||||
|
||||
@@ -487,7 +596,11 @@ class CombatService:
|
||||
if not session.is_in_combat():
|
||||
raise NotInCombatError("Session is not in combat")
|
||||
|
||||
encounter = session.combat_encounter
|
||||
# Get encounter from repository
|
||||
encounter = self.get_combat_state(session_id, user_id)
|
||||
if not encounter:
|
||||
raise NotInCombatError("Combat encounter not found")
|
||||
|
||||
current = encounter.get_current_combatant()
|
||||
|
||||
if not current:
|
||||
@@ -496,9 +609,55 @@ class CombatService:
|
||||
if current.is_player:
|
||||
raise InvalidActionError("Current combatant is a player, not an enemy")
|
||||
|
||||
# Check if the enemy is dead (shouldn't happen with fixed advance_turn, but defensive)
|
||||
if current.is_dead():
|
||||
# Skip this dead enemy's turn and advance
|
||||
self._advance_turn_and_save(encounter, session, user_id)
|
||||
next_combatant = encounter.get_current_combatant()
|
||||
return ActionResult(
|
||||
success=True,
|
||||
message=f"{current.name} is defeated and cannot act.",
|
||||
next_combatant_id=next_combatant.combatant_id if next_combatant else None,
|
||||
next_is_player=next_combatant.is_player if next_combatant else True,
|
||||
)
|
||||
|
||||
# Process start-of-turn effects
|
||||
turn_effects = encounter.start_turn()
|
||||
|
||||
# Check if enemy died from DoT effects at turn start
|
||||
if current.is_dead():
|
||||
# Check if combat ended
|
||||
combat_status = encounter.check_end_condition()
|
||||
if combat_status in [CombatStatus.VICTORY, CombatStatus.DEFEAT]:
|
||||
encounter.status = combat_status
|
||||
result = ActionResult(
|
||||
success=True,
|
||||
message=f"{current.name} was defeated by damage over time!",
|
||||
combat_ended=True,
|
||||
combat_status=combat_status,
|
||||
turn_effects=turn_effects,
|
||||
)
|
||||
if session.active_combat_encounter_id:
|
||||
self.combat_repository.end_encounter(
|
||||
encounter_id=session.active_combat_encounter_id,
|
||||
status=combat_status
|
||||
)
|
||||
session.active_combat_encounter_id = None
|
||||
session.combat_encounter = None
|
||||
self.session_service.update_session(session)
|
||||
return result
|
||||
else:
|
||||
# Advance past the dead enemy
|
||||
self._advance_turn_and_save(encounter, session, user_id)
|
||||
next_combatant = encounter.get_current_combatant()
|
||||
return ActionResult(
|
||||
success=True,
|
||||
message=f"{current.name} was defeated by damage over time!",
|
||||
next_combatant_id=next_combatant.combatant_id if next_combatant else None,
|
||||
next_is_player=next_combatant.is_player if next_combatant else True,
|
||||
turn_effects=turn_effects,
|
||||
)
|
||||
|
||||
# Check if stunned
|
||||
if current.is_stunned():
|
||||
result = ActionResult(
|
||||
@@ -539,14 +698,39 @@ class CombatService:
|
||||
if status != CombatStatus.ACTIVE:
|
||||
result.combat_ended = True
|
||||
result.combat_status = status
|
||||
session.end_combat()
|
||||
|
||||
# End encounter in repository
|
||||
if session.active_combat_encounter_id:
|
||||
self.combat_repository.end_encounter(
|
||||
encounter_id=session.active_combat_encounter_id,
|
||||
status=status
|
||||
)
|
||||
|
||||
# Clear session combat reference
|
||||
session.active_combat_encounter_id = None
|
||||
session.combat_encounter = None
|
||||
else:
|
||||
logger.info("Combat still active, advancing turn",
|
||||
session_id=session_id,
|
||||
encounter_id=encounter.encounter_id)
|
||||
self._advance_turn_and_save(encounter, session, user_id)
|
||||
next_combatant = encounter.get_current_combatant()
|
||||
result.next_combatant_id = next_combatant.combatant_id if next_combatant else None
|
||||
logger.info("Next combatant determined",
|
||||
next_combatant_id=next_combatant.combatant_id if next_combatant else None,
|
||||
next_is_player=next_combatant.is_player if next_combatant else None)
|
||||
if next_combatant:
|
||||
result.next_combatant_id = next_combatant.combatant_id
|
||||
result.next_is_player = next_combatant.is_player
|
||||
else:
|
||||
result.next_combatant_id = None
|
||||
result.next_is_player = True
|
||||
|
||||
self.session_service.update_session(session, user_id)
|
||||
self.session_service.update_session(session)
|
||||
|
||||
logger.info("Enemy turn complete, returning result",
|
||||
session_id=session_id,
|
||||
next_combatant_id=result.next_combatant_id,
|
||||
next_is_player=result.next_is_player)
|
||||
return result
|
||||
|
||||
# =========================================================================
|
||||
@@ -1146,9 +1330,25 @@ class CombatService:
|
||||
session,
|
||||
user_id: str
|
||||
) -> None:
|
||||
"""Advance the turn and save session state."""
|
||||
"""Advance the turn and save encounter state to repository."""
|
||||
logger.info("_advance_turn_and_save called",
|
||||
encounter_id=encounter.encounter_id,
|
||||
before_turn_index=encounter.current_turn_index,
|
||||
combat_log_count=len(encounter.combat_log))
|
||||
|
||||
encounter.advance_turn()
|
||||
|
||||
logger.info("Turn advanced, now saving",
|
||||
encounter_id=encounter.encounter_id,
|
||||
after_turn_index=encounter.current_turn_index,
|
||||
combat_log_count=len(encounter.combat_log))
|
||||
|
||||
# Save encounter to repository
|
||||
self.combat_repository.update_encounter(encounter)
|
||||
|
||||
logger.info("Encounter saved",
|
||||
encounter_id=encounter.encounter_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Global Instance
|
||||
|
||||
Reference in New Issue
Block a user