Remove redundant bare-truthy and .called checks where more specific content or entity validation already exists on subsequent lines.
277 lines
8.2 KiB
Python
277 lines
8.2 KiB
Python
"""Tests for stamina cue system wiring and reset behavior."""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
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 defender(test_zone):
|
|
"""Defender fixture 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_stamina_cue_after_combat_damage(player, defender):
|
|
"""check_stamina_cues is called after damage in combat resolution."""
|
|
import time
|
|
|
|
from mudlib.combat.encounter import CombatState
|
|
from mudlib.combat.engine import active_encounters, start_encounter
|
|
from mudlib.combat.moves import CombatMove
|
|
|
|
# Clear encounters
|
|
active_encounters.clear()
|
|
|
|
# Create a move that deals damage
|
|
move = CombatMove(
|
|
name="weak punch",
|
|
command="weak",
|
|
move_type="attack",
|
|
damage_pct=0.5,
|
|
stamina_cost=5.0,
|
|
hit_time_ms=100,
|
|
)
|
|
|
|
# Start encounter
|
|
encounter = start_encounter(player, defender)
|
|
encounter.attack(move)
|
|
|
|
# Fast-forward to RESOLVE state
|
|
encounter.state = CombatState.RESOLVE
|
|
encounter.move_started_at = time.monotonic()
|
|
|
|
# Patch check_stamina_cues to verify it's called
|
|
with patch("mudlib.combat.engine.check_stamina_cues") as mock_check:
|
|
from mudlib.combat.engine import process_combat
|
|
|
|
await process_combat()
|
|
|
|
# Should be called for both entities after damage
|
|
calls = [call[0][0] for call in mock_check.call_args_list]
|
|
assert player in calls
|
|
assert defender in calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stamina_cue_after_power_up_deduction(player):
|
|
"""check_stamina_cues is called during power-up stamina deduction."""
|
|
from mudlib.commands.power import power_up_loop
|
|
|
|
player.stamina = 100.0
|
|
player.max_stamina = 100.0
|
|
player.pl = 10.0
|
|
player.max_pl = 50.0
|
|
|
|
with patch("mudlib.commands.power.check_stamina_cues") as mock_check:
|
|
# Run power-up for a short time
|
|
task = asyncio.create_task(power_up_loop(player, target_pl=20.0))
|
|
await asyncio.sleep(0.15) # Let a few ticks happen
|
|
task.cancel()
|
|
with contextlib.suppress(asyncio.CancelledError):
|
|
await task
|
|
|
|
# check_stamina_cues should have been called at least once
|
|
calls = [call[0][0] for call in mock_check.call_args_list]
|
|
assert player in calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stamina_cue_after_attack_cost(player, defender):
|
|
"""check_stamina_cues is called after attack stamina cost deduction."""
|
|
from mudlib.combat.commands import do_attack
|
|
from mudlib.combat.engine import active_encounters
|
|
from mudlib.combat.moves import CombatMove
|
|
from mudlib.player import players
|
|
|
|
# Clear encounters
|
|
active_encounters.clear()
|
|
|
|
# Register defender in global players dict so do_attack can find it
|
|
players["Vegeta"] = defender
|
|
|
|
try:
|
|
# Create a move with high stamina cost
|
|
move = CombatMove(
|
|
name="power punch",
|
|
command="power",
|
|
move_type="attack",
|
|
damage_pct=0.3,
|
|
stamina_cost=60.0,
|
|
hit_time_ms=1000,
|
|
)
|
|
|
|
player.stamina = 100.0
|
|
player.max_stamina = 100.0
|
|
|
|
with patch("mudlib.combat.commands.check_stamina_cues") as mock_check:
|
|
await do_attack(player, "Vegeta", move)
|
|
|
|
# check_stamina_cues should have been called
|
|
calls = [call[0][0] for call in mock_check.call_args_list]
|
|
assert player in calls
|
|
finally:
|
|
players.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stamina_cue_after_defense_cost(player):
|
|
"""check_stamina_cues is called after defense stamina cost deduction."""
|
|
from mudlib.combat.commands import do_defend
|
|
from mudlib.combat.moves import CombatMove
|
|
|
|
# Create a defense move with stamina cost
|
|
move = CombatMove(
|
|
name="duck",
|
|
command="duck",
|
|
move_type="defense",
|
|
stamina_cost=30.0,
|
|
active_ms=100,
|
|
recovery_ms=2700,
|
|
)
|
|
|
|
player.stamina = 100.0
|
|
player.max_stamina = 100.0
|
|
|
|
with patch("mudlib.combat.commands.check_stamina_cues") as mock_check:
|
|
# Use a short active window to avoid blocking too long
|
|
move.active_ms = 10
|
|
move.recovery_ms = 10
|
|
await do_defend(player, "", move)
|
|
|
|
# check_stamina_cues should have been called
|
|
calls = [call[0][0] for call in mock_check.call_args_list]
|
|
assert player in calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stamina_cue_reset_on_resting_recovery(player):
|
|
"""_last_stamina_cue resets to 1.0 when stamina fully recovers via resting."""
|
|
from mudlib.combat.stamina import check_stamina_cues
|
|
from mudlib.player import players
|
|
from mudlib.resting import process_resting
|
|
|
|
# Register player in global dict so process_resting can find them
|
|
players[player.name] = player
|
|
|
|
try:
|
|
# Drop stamina to trigger a cue
|
|
player.stamina = 20.0
|
|
player.max_stamina = 100.0
|
|
await check_stamina_cues(player)
|
|
assert player._last_stamina_cue == 0.25 # Below 25% threshold
|
|
|
|
# Start resting and set stamina very close to max
|
|
player.resting = True
|
|
player.stamina = 99.85 # Will be clamped to 100.0 after one tick
|
|
|
|
# Process one tick to reach max
|
|
await process_resting()
|
|
|
|
# Stamina should be at max and _last_stamina_cue should reset
|
|
assert player.stamina == 100.0
|
|
assert player._last_stamina_cue == 1.0
|
|
finally:
|
|
players.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stamina_cue_reset_on_unconscious_recovery(player):
|
|
"""_last_stamina_cue resets to 1.0 when stamina recovers from unconsciousness."""
|
|
from mudlib.combat.stamina import check_stamina_cues
|
|
from mudlib.player import players
|
|
from mudlib.unconscious import process_unconscious
|
|
|
|
# Register player in global dict so process_unconscious can find them
|
|
players[player.name] = player
|
|
|
|
try:
|
|
# Drop stamina to trigger a cue
|
|
player.stamina = 5.0
|
|
player.max_stamina = 100.0
|
|
await check_stamina_cues(player)
|
|
assert player._last_stamina_cue == 0.10 # Below 10% threshold
|
|
|
|
# Drop to unconscious
|
|
player.stamina = 0.0
|
|
player.pl = 0.0
|
|
|
|
# Verify player is unconscious
|
|
assert player.posture == "unconscious"
|
|
|
|
# Process recovery tick to regain consciousness
|
|
await process_unconscious()
|
|
|
|
# Should have regained consciousness and reset cue
|
|
assert player.stamina > 0
|
|
assert player.pl > 0
|
|
assert player._last_stamina_cue == 1.0
|
|
finally:
|
|
players.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cue_fires_again_after_reset(player):
|
|
"""After reset, cues fire again on next descent."""
|
|
from mudlib.combat.stamina import check_stamina_cues
|
|
|
|
player.max_stamina = 100.0
|
|
|
|
# First descent: trigger 75% threshold
|
|
player.stamina = 70.0
|
|
player.writer.write.reset_mock()
|
|
await check_stamina_cues(player)
|
|
first_call_count = player.writer.write.call_count
|
|
assert first_call_count > 0
|
|
assert player._last_stamina_cue == 0.75
|
|
|
|
# Recover to max and reset
|
|
player.stamina = 100.0
|
|
player._last_stamina_cue = 1.0
|
|
|
|
# Second descent: should fire again at 75%
|
|
player.stamina = 70.0
|
|
player.writer.write.reset_mock()
|
|
await check_stamina_cues(player)
|
|
second_call_count = player.writer.write.call_count
|
|
assert second_call_count > 0 # Should fire again
|