Handle mob defeat in combat resolution

Phase 4: when combat ends, determine winner/loser. If the loser is a
Mob, despawn it and send a victory message to the winner. If the loser
is a Player fighting a Mob, send a defeat message instead.
This commit is contained in:
Jared Miller 2026-02-08 23:00:45 -05:00
parent f2d52c4936
commit 86c2c46dfa
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 124 additions and 1 deletions

View file

@ -3,7 +3,7 @@
import time
from mudlib.combat.encounter import IDLE_TIMEOUT, CombatEncounter, CombatState
from mudlib.entity import Entity
from mudlib.entity import Entity, Mob
# Global list of active combat encounters
active_encounters: list[CombatEncounter] = []
@ -101,6 +101,27 @@ async def process_combat() -> None:
await encounter.defender.send(result.defender_msg + "\r\n")
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

View file

@ -8,6 +8,7 @@ import pytest
import mudlib.commands.movement as movement_mod
from mudlib.combat import commands as combat_commands
from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import active_encounters, get_encounter
from mudlib.combat.moves import load_moves
from mudlib.entity import Mob
@ -406,3 +407,104 @@ class TestViewportRendering:
assert "*" not in stripped
look_mod.world = old
# --- Phase 4: mob defeat tests ---
class TestMobDefeat:
@pytest.fixture
def goblin_mob(self, goblin_toml):
template = load_mob_template(goblin_toml)
return spawn_mob(template, 0, 0)
@pytest.mark.asyncio
async def test_mob_despawned_on_pl_zero(
self, player, goblin_mob, punch_right
):
"""Mob with PL <= 0 gets despawned after combat resolves."""
from mudlib.combat.engine import process_combat, start_encounter
encounter = start_encounter(player, goblin_mob)
player.mode_stack.append("combat")
# Set mob PL very low so attack kills it
goblin_mob.pl = 1.0
# Attack and force resolution
encounter.attack(punch_right)
encounter.state = CombatState.RESOLVE
await process_combat()
assert goblin_mob not in mobs
assert goblin_mob.alive is False
@pytest.mark.asyncio
async def test_player_gets_victory_message(
self, player, goblin_mob, punch_right
):
"""Player receives a victory message when mob is defeated."""
from mudlib.combat.engine import process_combat, start_encounter
encounter = start_encounter(player, goblin_mob)
player.mode_stack.append("combat")
goblin_mob.pl = 1.0
encounter.attack(punch_right)
encounter.state = CombatState.RESOLVE
await process_combat()
messages = [
call[0][0] for call in player.writer.write.call_args_list
]
assert any("defeated" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_mob_stamina_depleted_despawns(
self, player, goblin_mob, punch_right
):
"""Mob is despawned when attacker stamina depleted (combat end)."""
from mudlib.combat.engine import process_combat, start_encounter
encounter = start_encounter(player, goblin_mob)
player.mode_stack.append("combat")
# Drain player stamina so combat ends on exhaustion
player.stamina = 0.0
encounter.attack(punch_right)
encounter.state = CombatState.RESOLVE
await process_combat()
# Encounter should have ended
assert get_encounter(player) is None
@pytest.mark.asyncio
async def test_player_defeat_not_despawned(
self, player, goblin_mob, punch_right
):
"""When player loses, player is not despawned."""
from mudlib.combat.engine import process_combat, start_encounter
# Mob attacks player — mob is attacker, player is defender
encounter = start_encounter(goblin_mob, player)
player.mode_stack.append("combat")
player.pl = 1.0
encounter.attack(punch_right)
encounter.state = CombatState.RESOLVE
await process_combat()
# Player should get defeat message, not be despawned
messages = [
call[0][0] for call in player.writer.write.call_args_list
]
assert any(
"defeated" in msg.lower() or "damage" in msg.lower()
for msg in messages
)
# Player is still in players dict (not removed)
assert player.name in players