Send Char.Vitals on combat resolution and rest completion

This commit is contained in:
Jared Miller 2026-02-11 22:55:46 -05:00
parent d253012122
commit e9f70ebd2f
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
5 changed files with 147 additions and 0 deletions

View file

@ -4,6 +4,7 @@ import time
from mudlib.combat.encounter import IDLE_TIMEOUT, CombatEncounter, CombatState from mudlib.combat.encounter import IDLE_TIMEOUT, CombatEncounter, CombatState
from mudlib.entity import Entity, Mob from mudlib.entity import Entity, Mob
from mudlib.gmcp import send_char_vitals
# Global list of active combat encounters # Global list of active combat encounters
active_encounters: list[CombatEncounter] = [] active_encounters: list[CombatEncounter] = []
@ -100,6 +101,13 @@ async def process_combat() -> None:
await encounter.attacker.send(result.attacker_msg + "\r\n") await encounter.attacker.send(result.attacker_msg + "\r\n")
await encounter.defender.send(result.defender_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: if result.combat_ended:
# Determine winner/loser # Determine winner/loser
if encounter.defender.pl <= 0: if encounter.defender.pl <= 0:

View file

@ -2,6 +2,7 @@
from mudlib.commands import CommandDefinition, register from mudlib.commands import CommandDefinition, register
from mudlib.entity import Entity from mudlib.entity import Entity
from mudlib.gmcp import send_map_data, send_room_info
from mudlib.player import Player from mudlib.player import Player
from mudlib.portal import Portal from mudlib.portal import Portal
from mudlib.zone import Zone from mudlib.zone import Zone
@ -106,6 +107,8 @@ async def move_player(player: Player, dx: int, dy: int, direction_name: str) ->
from mudlib.commands.look import cmd_look from mudlib.commands.look import cmd_look
await cmd_look(player, "") await cmd_look(player, "")
send_room_info(player)
send_map_data(player)
return # Don't do normal arrival+look return # Don't do normal arrival+look
else: else:
await player.send("The portal doesn't lead anywhere.\r\n") await player.send("The portal doesn't lead anywhere.\r\n")
@ -124,6 +127,8 @@ async def move_player(player: Player, dx: int, dy: int, direction_name: str) ->
from mudlib.commands.look import cmd_look from mudlib.commands.look import cmd_look
await cmd_look(player, "") await cmd_look(player, "")
send_room_info(player)
send_map_data(player)
async def send_nearby_message(entity: Entity, x: int, y: int, message: str) -> None: async def send_nearby_message(entity: Entity, x: int, y: int, message: str) -> None:

View file

@ -1,6 +1,7 @@
"""Resting system for stamina regeneration.""" """Resting system for stamina regeneration."""
from mudlib.commands.movement import send_nearby_message from mudlib.commands.movement import send_nearby_message
from mudlib.gmcp import send_char_vitals
from mudlib.player import players from mudlib.player import players
# Stamina regeneration rate: 2.0 per second # Stamina regeneration rate: 2.0 per second
@ -25,6 +26,7 @@ async def process_resting() -> None:
# Check if we reached max stamina # Check if we reached max stamina
if player.stamina >= player.max_stamina: if player.stamina >= player.max_stamina:
player.resting = False player.resting = False
send_char_vitals(player)
await player.send("You feel fully rested.\r\n") await player.send("You feel fully rested.\r\n")
await send_nearby_message( await send_nearby_message(
player, player.x, player.y, f"{player.name} stops resting.\r\n" player, player.x, player.y, f"{player.name} stops resting.\r\n"

View file

@ -246,3 +246,127 @@ def test_player_send_msdp_no_writer():
"""Test Player.send_msdp with no writer does not crash.""" """Test Player.send_msdp with no writer does not crash."""
p = Player(name="NoWriter", writer=None) p = Player(name="NoWriter", writer=None)
p.send_msdp({}) # Should not raise p.send_msdp({}) # Should not raise
@pytest.mark.asyncio
async def test_room_info_sent_on_movement(player, test_zone):
"""Test Room.Info is sent when player moves."""
from mudlib.commands.movement import move_player
await move_player(player, 1, 0, "east")
# Verify send_gmcp was called with Room.Info
gmcp_calls = player.writer.send_gmcp.call_args_list
room_info_calls = [call for call in gmcp_calls if call[0][0] == "Room.Info"]
assert len(room_info_calls) > 0, "Room.Info should be sent on movement"
@pytest.mark.asyncio
async def test_map_data_sent_on_movement(player, test_zone):
"""Test Room.Map is sent when player moves."""
from mudlib.commands.movement import move_player
await move_player(player, 1, 0, "east")
# Verify send_gmcp was called with Room.Map
gmcp_calls = player.writer.send_gmcp.call_args_list
room_map_calls = [call for call in gmcp_calls if call[0][0] == "Room.Map"]
assert len(room_map_calls) > 0, "Room.Map should be sent on movement"
@pytest.mark.asyncio
async def test_char_vitals_sent_on_rest_complete(player):
"""Test Char.Vitals is sent when resting completes."""
from mudlib.resting import process_resting
player.resting = True
player.stamina = player.max_stamina - 0.1 # Almost full
await process_resting()
# Should have called send_gmcp with Char.Vitals
player.writer.send_gmcp.assert_called_once_with(
"Char.Vitals",
{
"pl": round(player.pl, 1),
"stamina": round(player.max_stamina, 1),
"max_stamina": round(player.max_stamina, 1),
},
)
@pytest.mark.asyncio
async def test_char_vitals_sent_on_combat_resolve():
"""Test Char.Vitals is sent to both players on combat resolution."""
import time
from mudlib.combat.engine import active_encounters, process_combat, start_encounter
from mudlib.combat.moves import CombatMove
# Clear encounters
active_encounters.clear()
# Create two players with mock writers
mock_writer_1 = MagicMock()
mock_writer_1.write = MagicMock()
mock_writer_1.drain = AsyncMock()
mock_writer_1.send_gmcp = MagicMock()
mock_writer_2 = MagicMock()
mock_writer_2.write = MagicMock()
mock_writer_2.drain = AsyncMock()
mock_writer_2.send_gmcp = MagicMock()
attacker = Player(
name="Goku", x=0, y=0, pl=100.0, stamina=50.0, writer=mock_writer_1
)
defender = Player(
name="Vegeta", x=0, y=0, pl=100.0, stamina=50.0, writer=mock_writer_2
)
attacker.mode_stack.append("combat")
defender.mode_stack.append("combat")
# Create encounter and attack
encounter = start_encounter(attacker, defender)
punch = CombatMove(
name="punch right",
move_type="attack",
stamina_cost=5.0,
timing_window_ms=800,
damage_pct=0.15,
countered_by=["dodge left"],
)
encounter.attack(punch)
# Advance past telegraph and window to trigger resolution
time.sleep(0.31)
await process_combat()
time.sleep(0.85)
# Reset mocks before the resolution call
mock_writer_1.send_gmcp.reset_mock()
mock_writer_2.send_gmcp.reset_mock()
await process_combat()
# Both players should have received Char.Vitals
assert mock_writer_1.send_gmcp.call_count == 1
assert mock_writer_2.send_gmcp.call_count == 1
# Check attacker's call
attacker_call = mock_writer_1.send_gmcp.call_args[0]
assert attacker_call[0] == "Char.Vitals"
assert "pl" in attacker_call[1]
assert "stamina" in attacker_call[1]
assert "max_stamina" in attacker_call[1]
# Check defender's call
defender_call = mock_writer_2.send_gmcp.call_args[0]
assert defender_call[0] == "Char.Vitals"
assert "pl" in defender_call[1]
assert "stamina" in defender_call[1]
assert "max_stamina" in defender_call[1]
# Cleanup
active_encounters.clear()

View file

@ -42,6 +42,14 @@ class MockWriter:
async def drain(self): async def drain(self):
pass pass
def send_gmcp(self, package: str, data: dict):
"""Mock GMCP send (no-op for paint mode tests)."""
pass
def send_msdp(self, data: dict):
"""Mock MSDP send (no-op for paint mode tests)."""
pass
def get_output(self) -> str: def get_output(self) -> str:
return "".join(self.messages) return "".join(self.messages)