Send Char.Status on combat end and rest state changes
This commit is contained in:
parent
e9f70ebd2f
commit
e247d70612
5 changed files with 140 additions and 2 deletions
|
|
@ -4,7 +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
|
from mudlib.gmcp import send_char_status, send_char_vitals
|
||||||
|
|
||||||
# Global list of active combat encounters
|
# Global list of active combat encounters
|
||||||
active_encounters: list[CombatEncounter] = []
|
active_encounters: list[CombatEncounter] = []
|
||||||
|
|
@ -86,6 +86,7 @@ async def process_combat() -> None:
|
||||||
for entity in (encounter.attacker, encounter.defender):
|
for entity in (encounter.attacker, encounter.defender):
|
||||||
if isinstance(entity, Player) and entity.mode == "combat":
|
if isinstance(entity, Player) and entity.mode == "combat":
|
||||||
entity.mode_stack.pop()
|
entity.mode_stack.pop()
|
||||||
|
send_char_status(entity)
|
||||||
|
|
||||||
end_encounter(encounter)
|
end_encounter(encounter)
|
||||||
continue
|
continue
|
||||||
|
|
@ -134,10 +135,12 @@ async def process_combat() -> None:
|
||||||
attacker = encounter.attacker
|
attacker = encounter.attacker
|
||||||
if isinstance(attacker, Player) and attacker.mode == "combat":
|
if isinstance(attacker, Player) and attacker.mode == "combat":
|
||||||
attacker.mode_stack.pop()
|
attacker.mode_stack.pop()
|
||||||
|
send_char_status(attacker)
|
||||||
|
|
||||||
defender = encounter.defender
|
defender = encounter.defender
|
||||||
if isinstance(defender, Player) and defender.mode == "combat":
|
if isinstance(defender, Player) and defender.mode == "combat":
|
||||||
defender.mode_stack.pop()
|
defender.mode_stack.pop()
|
||||||
|
send_char_status(defender)
|
||||||
|
|
||||||
# Remove encounter from active list
|
# Remove encounter from active list
|
||||||
end_encounter(encounter)
|
end_encounter(encounter)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Rest command for restoring stamina."""
|
"""Rest command for restoring stamina."""
|
||||||
|
|
||||||
from mudlib.commands.movement import send_nearby_message
|
from mudlib.commands.movement import send_nearby_message
|
||||||
|
from mudlib.gmcp import send_char_status
|
||||||
from mudlib.player import Player
|
from mudlib.player import Player
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ async def cmd_rest(player: Player, args: str) -> None:
|
||||||
if player.resting:
|
if player.resting:
|
||||||
# Stop resting
|
# Stop resting
|
||||||
player.resting = False
|
player.resting = False
|
||||||
|
send_char_status(player)
|
||||||
await player.send("You stop resting.\r\n")
|
await player.send("You stop resting.\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"
|
||||||
|
|
@ -27,6 +29,7 @@ async def cmd_rest(player: Player, args: str) -> None:
|
||||||
else:
|
else:
|
||||||
# Start resting
|
# Start resting
|
||||||
player.resting = True
|
player.resting = True
|
||||||
|
send_char_status(player)
|
||||||
await player.send("You begin to rest.\r\n")
|
await player.send("You begin to rest.\r\n")
|
||||||
await send_nearby_message(
|
await send_nearby_message(
|
||||||
player, player.x, player.y, f"{player.name} begins to rest.\r\n"
|
player, player.x, player.y, f"{player.name} begins to rest.\r\n"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +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.gmcp import send_char_status, 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
|
||||||
|
|
@ -26,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_status(player)
|
||||||
send_char_vitals(player)
|
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(
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ from mudlib.combat.commands import register_combat_commands
|
||||||
from mudlib.combat.engine import process_combat
|
from mudlib.combat.engine import process_combat
|
||||||
from mudlib.content import load_commands
|
from mudlib.content import load_commands
|
||||||
from mudlib.effects import clear_expired
|
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.if_session import broadcast_to_spectators
|
||||||
from mudlib.mob_ai import process_mobs
|
from mudlib.mob_ai import process_mobs
|
||||||
from mudlib.mobs import load_mob_templates, mob_templates
|
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."""
|
"""Run periodic game tasks at TICK_RATE ticks per second."""
|
||||||
log.info("game loop started (%d ticks/sec)", TICK_RATE)
|
log.info("game loop started (%d ticks/sec)", TICK_RATE)
|
||||||
last_save_time = time.monotonic()
|
last_save_time = time.monotonic()
|
||||||
|
tick_count = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
t0 = asyncio.get_event_loop().time()
|
t0 = asyncio.get_event_loop().time()
|
||||||
|
tick_count += 1
|
||||||
clear_expired()
|
clear_expired()
|
||||||
await process_combat()
|
await process_combat()
|
||||||
await process_mobs(mudlib.combat.commands.combat_moves)
|
await process_mobs(mudlib.combat.commands.combat_moves)
|
||||||
await process_resting()
|
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)
|
# Periodic auto-save (every 60 seconds)
|
||||||
current_time = time.monotonic()
|
current_time = time.monotonic()
|
||||||
if current_time - last_save_time >= AUTOSAVE_INTERVAL:
|
if current_time - last_save_time >= AUTOSAVE_INTERVAL:
|
||||||
|
|
@ -347,6 +361,12 @@ async def shell(
|
||||||
# Show initial map
|
# Show initial map
|
||||||
await mudlib.commands.look.cmd_look(player, "")
|
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
|
# Command loop
|
||||||
try:
|
try:
|
||||||
while not _writer.is_closing():
|
while not _writer.is_closing():
|
||||||
|
|
|
||||||
|
|
@ -370,3 +370,114 @@ async def test_char_vitals_sent_on_combat_resolve():
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
active_encounters.clear()
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue