Combat engine looks up mob template loot table and passes to create_corpse. Goblin template now drops crude club (80%) and 1-3 gold coins (50%).
207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
"""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)
|