Send Char.Vitals on combat resolution and rest completion
This commit is contained in:
parent
d253012122
commit
e9f70ebd2f
5 changed files with 147 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ 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] = []
|
||||
|
|
@ -100,6 +101,13 @@ async def process_combat() -> None:
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from mudlib.commands import CommandDefinition, register
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.gmcp import send_map_data, send_room_info
|
||||
from mudlib.player import Player
|
||||
from mudlib.portal import Portal
|
||||
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
|
||||
|
||||
await cmd_look(player, "")
|
||||
send_room_info(player)
|
||||
send_map_data(player)
|
||||
return # Don't do normal arrival+look
|
||||
else:
|
||||
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
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Resting system for stamina regeneration."""
|
||||
|
||||
from mudlib.commands.movement import send_nearby_message
|
||||
from mudlib.gmcp import send_char_vitals
|
||||
from mudlib.player import players
|
||||
|
||||
# Stamina regeneration rate: 2.0 per second
|
||||
|
|
@ -25,6 +26,7 @@ async def process_resting() -> None:
|
|||
# Check if we reached max stamina
|
||||
if player.stamina >= player.max_stamina:
|
||||
player.resting = False
|
||||
send_char_vitals(player)
|
||||
await player.send("You feel fully rested.\r\n")
|
||||
await send_nearby_message(
|
||||
player, player.x, player.y, f"{player.name} stops resting.\r\n"
|
||||
|
|
|
|||
|
|
@ -246,3 +246,127 @@ def test_player_send_msdp_no_writer():
|
|||
"""Test Player.send_msdp with no writer does not crash."""
|
||||
p = Player(name="NoWriter", writer=None)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ class MockWriter:
|
|||
async def drain(self):
|
||||
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:
|
||||
return "".join(self.messages)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue