"""Combat encounter management and processing.""" import time from mudlib.combat.encounter import IDLE_TIMEOUT, CombatEncounter, CombatState from mudlib.combat.stamina import check_stamina_cues from mudlib.entity import Entity, Mob from mudlib.gmcp import send_char_status, send_char_vitals from mudlib.render.colors import colorize from mudlib.render.pov import render_pov # Global list of active combat encounters active_encounters: list[CombatEncounter] = [] def start_encounter(attacker: Entity, defender: Entity) -> CombatEncounter: """Start a new combat encounter. Args: attacker: The entity initiating combat defender: The target entity Returns: The created CombatEncounter Raises: ValueError: If either entity is already in combat """ # Check if either entity is already in combat if get_encounter(attacker) is not None: msg = f"{attacker.name} is already in combat" raise ValueError(msg) if get_encounter(defender) is not None: msg = f"{defender.name} is already in combat" raise ValueError(msg) # Create and register the encounter encounter = CombatEncounter( attacker=attacker, defender=defender, last_action_at=time.monotonic(), ) active_encounters.append(encounter) # Cancel any active power-up tasks for entity in (attacker, defender): task = getattr(entity, "_power_task", None) if task is not None: task.cancel() if hasattr(entity, "_power_task"): entity._power_task = None # type: ignore[attr-defined] return encounter def get_encounter(entity: Entity) -> CombatEncounter | None: """Find the active encounter for an entity. Args: entity: The entity to search for Returns: CombatEncounter if entity is in combat, None otherwise """ for encounter in active_encounters: if encounter.attacker is entity or encounter.defender is entity: return encounter return None def end_encounter(encounter: CombatEncounter) -> None: """End and remove an encounter from the active list. Args: encounter: The encounter to end """ if encounter in active_encounters: active_encounters.remove(encounter) async def process_combat() -> None: """Process all active combat encounters. This should be called each game loop tick to advance combat state machines. """ now = time.monotonic() for encounter in active_encounters[:]: # Copy list to allow modification # Check for idle timeout if now - encounter.last_action_at > IDLE_TIMEOUT: await encounter.attacker.send("Combat has fizzled out.\r\n") await encounter.defender.send("Combat has fizzled out.\r\n") from mudlib.player import Player for entity in (encounter.attacker, encounter.defender): if isinstance(entity, Player) and entity.mode == "combat": entity.mode_stack.pop() send_char_status(entity) end_encounter(encounter) continue # Save previous state to detect transitions previous_state = encounter.state # Tick the state machine encounter.tick(now) # Send announce message on TELEGRAPH → WINDOW transition if ( previous_state == CombatState.TELEGRAPH and encounter.state == CombatState.WINDOW and encounter.current_move and encounter.current_move.announce ): template = encounter.current_move.announce announce_color = encounter.current_move.announce_color for viewer in (encounter.attacker, encounter.defender): msg = render_pov( template, viewer, encounter.attacker, encounter.defender ) if announce_color: color_depth = getattr(viewer, "color_depth", None) msg = colorize(f"{{{announce_color}}}{msg}{{/}}", color_depth) await viewer.send(msg + "\r\n") # Auto-resolve if in RESOLVE state if encounter.state == CombatState.RESOLVE: # Save current_move before resolve() clears it current_move = encounter.current_move result = encounter.resolve() # Send resolution messages to both participants using POV resolve_color = current_move.resolve_color if current_move else "bold" for viewer in (encounter.attacker, encounter.defender): msg = render_pov( result.resolve_template, viewer, encounter.attacker, encounter.defender, ) if resolve_color: color_depth = getattr(viewer, "color_depth", None) msg = colorize(f"{{{resolve_color}}}{msg}{{/}}", color_depth) await viewer.send(msg + "\r\n") # Send vitals update after damage resolution from mudlib.player import Player for entity in (encounter.attacker, encounter.defender): if isinstance(entity, Player): send_char_vitals(entity) # Check stamina cues after damage await check_stamina_cues(encounter.attacker) await check_stamina_cues(encounter.defender) if result.combat_ended: # Determine winner/loser if encounter.defender.pl <= 0: loser = encounter.defender winner = encounter.attacker else: loser = encounter.attacker winner = encounter.defender # Despawn mob losers, send victory/defeat messages if isinstance(loser, Mob): from mudlib.corpse import create_corpse from mudlib.mobs import mob_templates from mudlib.zone import Zone zone = loser.location if isinstance(zone, Zone): # Look up loot table from mob template template = mob_templates.get(loser.name) loot_table = template.loot if template else None create_corpse(loser, zone, loot_table=loot_table) else: from mudlib.mobs import despawn_mob despawn_mob(loser) await winner.send(f"You have defeated the {loser.name}!\r\n") elif isinstance(winner, Mob): await loser.send( f"You have been defeated by the {winner.name}!\r\n" ) # Pop combat mode from both entities if they're Players from mudlib.player import Player attacker = encounter.attacker if isinstance(attacker, Player) and attacker.mode == "combat": attacker.mode_stack.pop() send_char_status(attacker) defender = encounter.defender if isinstance(defender, Player) and defender.mode == "combat": defender.mode_stack.pop() send_char_status(defender) # Remove encounter from active list end_encounter(encounter)