mud/src/mudlib/combat/engine.py

143 lines
4.6 KiB
Python

"""Combat encounter management and processing."""
import time
from mudlib.combat.encounter import IDLE_TIMEOUT, CombatEncounter, CombatState
from mudlib.entity import Entity, Mob
from mudlib.gmcp import send_char_vitals
# 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)
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()
end_encounter(encounter)
continue
# Tick the state machine
encounter.tick(now)
# Auto-resolve if in RESOLVE state
if encounter.state == CombatState.RESOLVE:
result = encounter.resolve()
# Send resolution messages to both participants
await encounter.attacker.send(result.attacker_msg + "\r\n")
await encounter.defender.send(result.defender_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)
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.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()
defender = encounter.defender
if isinstance(defender, Player) and defender.mode == "combat":
defender.mode_stack.pop()
# Remove encounter from active list
end_encounter(encounter)