"""Combat encounter and state machine.""" import time from dataclasses import dataclass from enum import Enum from mudlib.combat.moves import CombatMove from mudlib.entity import Entity class CombatState(Enum): """States of the combat state machine.""" IDLE = "idle" TELEGRAPH = "telegraph" WINDOW = "window" RESOLVE = "resolve" # Telegraph phase duration in seconds (3 game ticks at 100ms/tick) TELEGRAPH_DURATION = 0.3 @dataclass class CombatEncounter: """Represents an active combat encounter between two entities.""" attacker: Entity defender: Entity state: CombatState = CombatState.IDLE current_move: CombatMove | None = None move_started_at: float = 0.0 pending_defense: CombatMove | None = None def attack(self, move: CombatMove) -> None: """Initiate an attack move. Args: move: The attack move to execute """ self.current_move = move self.state = CombatState.TELEGRAPH self.move_started_at = time.monotonic() # Apply stamina cost self.attacker.stamina -= move.stamina_cost def defend(self, move: CombatMove) -> None: """Queue a defense move. Args: move: The defense move to attempt """ self.pending_defense = move # Apply stamina cost self.defender.stamina -= move.stamina_cost def tick(self, now: float) -> None: """Advance the state machine based on current time. Args: now: Current time from monotonic clock """ if self.state == CombatState.TELEGRAPH: # Check if telegraph phase is over elapsed = now - self.move_started_at if elapsed >= TELEGRAPH_DURATION: self.state = CombatState.WINDOW elif self.state == CombatState.WINDOW: # Check if timing window has expired if self.current_move is None: return elapsed = now - self.move_started_at window_seconds = self.current_move.timing_window_ms / 1000.0 total_time = TELEGRAPH_DURATION + window_seconds if elapsed >= total_time: self.state = CombatState.RESOLVE def resolve(self) -> tuple[str, bool]: """Resolve the combat exchange and return result message. Returns: Tuple of (result message, combat_ended flag) """ if self.current_move is None: return ("No active move to resolve.", False) # Check if defense counters attack defense_succeeds = ( self.pending_defense and self.pending_defense.name in self.current_move.countered_by ) if defense_succeeds: # Successful counter - no damage result = f"{self.defender.name} countered the attack!" elif self.pending_defense: # Wrong defense - normal damage damage = self.attacker.pl * self.current_move.damage_pct self.defender.pl -= damage result = ( f"{self.attacker.name} hit {self.defender.name} " f"for {damage:.1f} damage!" ) else: # No defense - increased damage damage = self.attacker.pl * self.current_move.damage_pct * 1.5 self.defender.pl -= damage result = ( f"{self.defender.name} took the hit full force for {damage:.1f} damage!" ) # Check for combat end conditions combat_ended = self.defender.pl <= 0 or self.attacker.stamina <= 0 # Reset to IDLE self.state = CombatState.IDLE self.current_move = None self.pending_defense = None return (result, combat_ended)