Send Char.Status on combat end and rest state changes

This commit is contained in:
Jared Miller 2026-02-11 23:00:18 -05:00
parent e9f70ebd2f
commit e247d70612
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
5 changed files with 140 additions and 2 deletions

View file

@ -4,7 +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
from mudlib.gmcp import send_char_status, send_char_vitals
# Global list of active combat encounters
active_encounters: list[CombatEncounter] = []
@ -86,6 +86,7 @@ async def process_combat() -> None:
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
@ -134,10 +135,12 @@ async def process_combat() -> None:
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)

View file

@ -1,6 +1,7 @@
"""Rest command for restoring stamina."""
from mudlib.commands.movement import send_nearby_message
from mudlib.gmcp import send_char_status
from mudlib.player import Player
@ -20,6 +21,7 @@ async def cmd_rest(player: Player, args: str) -> None:
if player.resting:
# Stop resting
player.resting = False
send_char_status(player)
await player.send("You stop resting.\r\n")
await send_nearby_message(
player, player.x, player.y, f"{player.name} stops resting.\r\n"
@ -27,6 +29,7 @@ async def cmd_rest(player: Player, args: str) -> None:
else:
# Start resting
player.resting = True
send_char_status(player)
await player.send("You begin to rest.\r\n")
await send_nearby_message(
player, player.x, player.y, f"{player.name} begins to rest.\r\n"

View file

@ -1,7 +1,7 @@
"""Resting system for stamina regeneration."""
from mudlib.commands.movement import send_nearby_message
from mudlib.gmcp import send_char_vitals
from mudlib.gmcp import send_char_status, send_char_vitals
from mudlib.player import players
# Stamina regeneration rate: 2.0 per second
@ -26,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_status(player)
send_char_vitals(player)
await player.send("You feel fully rested.\r\n")
await send_nearby_message(

View file

@ -32,6 +32,13 @@ from mudlib.combat.commands import register_combat_commands
from mudlib.combat.engine import process_combat
from mudlib.content import load_commands
from mudlib.effects import clear_expired
from mudlib.gmcp import (
send_char_status,
send_char_vitals,
send_map_data,
send_msdp_vitals,
send_room_info,
)
from mudlib.if_session import broadcast_to_spectators
from mudlib.mob_ai import process_mobs
from mudlib.mobs import load_mob_templates, mob_templates
@ -77,14 +84,21 @@ async def game_loop() -> None:
"""Run periodic game tasks at TICK_RATE ticks per second."""
log.info("game loop started (%d ticks/sec)", TICK_RATE)
last_save_time = time.monotonic()
tick_count = 0
while True:
t0 = asyncio.get_event_loop().time()
tick_count += 1
clear_expired()
await process_combat()
await process_mobs(mudlib.combat.commands.combat_moves)
await process_resting()
# MSDP updates once per second (every TICK_RATE ticks)
if tick_count % TICK_RATE == 0:
for p in list(players.values()):
send_msdp_vitals(p)
# Periodic auto-save (every 60 seconds)
current_time = time.monotonic()
if current_time - last_save_time >= AUTOSAVE_INTERVAL:
@ -347,6 +361,12 @@ async def shell(
# Show initial map
await mudlib.commands.look.cmd_look(player, "")
# Send initial GMCP data to rich clients
send_char_vitals(player)
send_char_status(player)
send_room_info(player)
send_map_data(player)
# Command loop
try:
while not _writer.is_closing():

View file

@ -370,3 +370,114 @@ async def test_char_vitals_sent_on_combat_resolve():
# Cleanup
active_encounters.clear()
@pytest.mark.asyncio
async def test_char_status_sent_on_combat_end():
"""Test Char.Status is sent when combat ends (victory/defeat)."""
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=1.0, stamina=50.0, writer=mock_writer_2
)
attacker.mode_stack.append("combat")
defender.mode_stack.append("combat")
# Create encounter and attack (will kill defender)
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.Status (after Char.Vitals)
# Check that Char.Status was called at least once
attacker_calls = [
call for call in mock_writer_1.send_gmcp.call_args_list if call[0][0] == "Char.Status"
]
defender_calls = [
call for call in mock_writer_2.send_gmcp.call_args_list if call[0][0] == "Char.Status"
]
assert len(attacker_calls) >= 1, "Attacker should receive Char.Status on combat end"
assert len(defender_calls) >= 1, "Defender should receive Char.Status on combat end"
# Verify mode is back to normal for attacker
assert attacker_calls[0][0][1]["mode"] == "normal"
assert attacker_calls[0][0][1]["in_combat"] is False
# Cleanup
active_encounters.clear()
@pytest.mark.asyncio
async def test_char_status_sent_on_rest_start(player):
"""Test Char.Status is sent when resting begins."""
from mudlib.commands.rest import cmd_rest
player.stamina = 50.0
player.resting = False
await cmd_rest(player, "")
# Check that Char.Status was sent
status_calls = [
call for call in player.writer.send_gmcp.call_args_list if call[0][0] == "Char.Status"
]
assert len(status_calls) == 1
assert status_calls[0][0][1]["resting"] is True
@pytest.mark.asyncio
async def test_char_status_sent_on_rest_complete(player):
"""Test Char.Status 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()
# Check that Char.Status was sent
status_calls = [
call for call in player.writer.send_gmcp.call_args_list if call[0][0] == "Char.Status"
]
assert len(status_calls) == 1
assert status_calls[0][0][1]["resting"] is False