143 lines
4.6 KiB
Python
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)
|