"""Tests for stamina cue broadcasts.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from mudlib.player import Player from mudlib.zone import Zone @pytest.fixture def test_zone(): """Override conftest test_zone with smaller zone for stamina tests.""" return Zone(name="test", width=10, height=10) @pytest.fixture def player(mock_writer, test_zone): """Override conftest player with custom position and stats for stamina tests.""" p = Player( name="Goku", x=5, y=5, pl=100.0, stamina=100.0, max_stamina=100.0, writer=mock_writer, ) p.move_to(test_zone, x=5, y=5) return p @pytest.fixture def nearby_player(test_zone): """Override conftest nearby_player with custom position for stamina tests.""" w = MagicMock() w.write = MagicMock() w.drain = AsyncMock() p = Player( name="Vegeta", x=5, y=5, pl=100.0, stamina=100.0, max_stamina=100.0, writer=w, ) p.move_to(test_zone, x=5, y=5) return p @pytest.mark.asyncio async def test_no_cue_above_75_percent(player): """No cue when stamina is above 75%.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 80.0 player.max_stamina = 100.0 await check_stamina_cues(player) # No message sent to player player.writer.write.assert_not_called() @pytest.mark.asyncio async def test_breathing_heavily_below_75_percent(player): """'Breathing heavily' cue when stamina drops below 75%.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 70.0 player.max_stamina = 100.0 await check_stamina_cues(player) # Check self-directed message calls = [call[0][0] for call in player.writer.write.call_args_list] assert any("You're breathing heavily." in msg for msg in calls) @pytest.mark.asyncio async def test_drenched_in_sweat_below_50_percent(player): """'Drenched in sweat' cue when below 50%.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 45.0 player.max_stamina = 100.0 await check_stamina_cues(player) calls = [call[0][0] for call in player.writer.write.call_args_list] assert any("You're drenched in sweat." in msg for msg in calls) @pytest.mark.asyncio async def test_visibly_shaking_below_25_percent(player): """'Visibly shaking' cue when below 25%.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 20.0 player.max_stamina = 100.0 await check_stamina_cues(player) calls = [call[0][0] for call in player.writer.write.call_args_list] assert any("You're visibly shaking from exhaustion." in msg for msg in calls) @pytest.mark.asyncio async def test_can_barely_stand_below_10_percent(player): """'Can barely stand' cue when below 10%.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 8.0 player.max_stamina = 100.0 await check_stamina_cues(player) calls = [call[0][0] for call in player.writer.write.call_args_list] assert any("You can barely stand." in msg for msg in calls) @pytest.mark.asyncio async def test_same_threshold_not_triggered_twice(player): """Same threshold doesn't trigger twice (spam prevention).""" from mudlib.combat.stamina import check_stamina_cues # First trigger at 70% player.stamina = 70.0 player.max_stamina = 100.0 await check_stamina_cues(player) first_call_count = player.writer.write.call_count # Second trigger at same threshold (still 70%) player.stamina = 70.0 await check_stamina_cues(player) second_call_count = player.writer.write.call_count # No new messages should have been sent assert second_call_count == first_call_count @pytest.mark.asyncio async def test_higher_threshold_doesnt_retrigger(player): """Higher threshold doesn't trigger if already at lower.""" from mudlib.combat.stamina import check_stamina_cues # Drop to 20% first player.stamina = 20.0 player.max_stamina = 100.0 await check_stamina_cues(player) player.writer.write.reset_mock() # Recover to 30% (back into 25-50% range) player.stamina = 30.0 await check_stamina_cues(player) # No new cue should trigger player.writer.write.assert_not_called() @pytest.mark.asyncio async def test_self_directed_message_sent(player): """Self-directed message sent to the entity.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 70.0 player.max_stamina = 100.0 await check_stamina_cues(player) # Verify self message was sent calls = [call[0][0] for call in player.writer.write.call_args_list] assert any("You're breathing heavily." in msg for msg in calls) @pytest.mark.asyncio async def test_nearby_broadcast_sent(player, nearby_player): """Nearby broadcast sent to other players.""" from mudlib.combat.stamina import check_stamina_cues player.stamina = 70.0 player.max_stamina = 100.0 with patch("mudlib.combat.stamina.send_nearby_message") as mock_nearby: await check_stamina_cues(player) # Verify send_nearby_message was called mock_nearby.assert_called_once() args = mock_nearby.call_args[0] assert args[0] is player assert args[1] == 5 # x coordinate assert args[2] == 5 # y coordinate assert "Goku is breathing heavily." in args[3] @pytest.mark.asyncio async def test_descending_thresholds_trigger_each_once(player): """Descending through thresholds triggers each once.""" from mudlib.combat.stamina import check_stamina_cues player.max_stamina = 100.0 # Start at 80%, no cue player.stamina = 80.0 await check_stamina_cues(player) count_80 = player.writer.write.call_count # Drop to 70%, should trigger 75% threshold player.stamina = 70.0 await check_stamina_cues(player) count_70 = player.writer.write.call_count assert count_70 > count_80 # Drop to 40%, should trigger 50% threshold player.stamina = 40.0 await check_stamina_cues(player) count_40 = player.writer.write.call_count assert count_40 > count_70 # Drop to 20%, should trigger 25% threshold player.stamina = 20.0 await check_stamina_cues(player) count_20 = player.writer.write.call_count assert count_20 > count_40 # Drop to 5%, should trigger 10% threshold player.stamina = 5.0 await check_stamina_cues(player) count_5 = player.writer.write.call_count assert count_5 > count_20