Players become unconscious when PL or stamina drops to 0. While unconscious, both stats slowly recover at 0.1 per tick (1.0 per second). When both reach above 0, player regains consciousness with a message. Recovery runs in the main game loop via process_unconscious.
259 lines
7.4 KiB
Python
259 lines
7.4 KiB
Python
"""Tests for unconscious state mechanics."""
|
|
|
|
import pytest
|
|
|
|
from mudlib.combat.engine import active_encounters, start_encounter
|
|
from mudlib.combat.moves import CombatMove
|
|
from mudlib.player import Player, players
|
|
from mudlib.unconscious import process_unconscious
|
|
|
|
|
|
class MockWriter:
|
|
"""Mock writer that captures output."""
|
|
|
|
def __init__(self):
|
|
self.written = []
|
|
self.closed = False
|
|
|
|
# Mock option negotiation state
|
|
from unittest.mock import MagicMock
|
|
|
|
self.local_option = MagicMock()
|
|
self.local_option.enabled = MagicMock(return_value=False)
|
|
self.remote_option = MagicMock()
|
|
self.remote_option.enabled = MagicMock(return_value=False)
|
|
|
|
def write(self, data):
|
|
if isinstance(data, str):
|
|
self.written.append(data)
|
|
|
|
async def drain(self):
|
|
pass
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
def is_closing(self):
|
|
return self.closed
|
|
|
|
|
|
@pytest.fixture
|
|
def clear_state():
|
|
"""Clear global state before each test."""
|
|
players.clear()
|
|
active_encounters.clear()
|
|
yield
|
|
players.clear()
|
|
active_encounters.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
"""Create a mock writer for testing."""
|
|
return MockWriter()
|
|
|
|
|
|
def test_unconscious_when_pl_zero(clear_state, mock_writer):
|
|
"""Test that entity becomes unconscious when PL reaches 0."""
|
|
player = Player(name="TestPlayer", x=0, y=0, pl=100.0, writer=mock_writer)
|
|
|
|
# Player should start conscious
|
|
assert player.posture != "unconscious"
|
|
|
|
# Drop PL to 0
|
|
player.pl = 0.0
|
|
|
|
# Player should now be unconscious
|
|
assert player.posture == "unconscious"
|
|
|
|
|
|
def test_unconscious_when_stamina_zero(clear_state, mock_writer):
|
|
"""Test that entity becomes unconscious when stamina reaches 0."""
|
|
player = Player(name="TestPlayer", x=0, y=0, stamina=100.0, writer=mock_writer)
|
|
|
|
# Player should start conscious
|
|
assert player.posture != "unconscious"
|
|
|
|
# Drop stamina to 0
|
|
player.stamina = 0.0
|
|
|
|
# Player should now be unconscious
|
|
assert player.posture == "unconscious"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unconscious_recovery(clear_state, mock_writer):
|
|
"""Test that unconscious player recovers after ticks."""
|
|
player = Player(
|
|
name="TestPlayer", x=0, y=0, pl=0.0, stamina=0.0, writer=mock_writer
|
|
)
|
|
players["TestPlayer"] = player
|
|
|
|
# Player should start unconscious
|
|
assert player.posture == "unconscious"
|
|
|
|
# Process recovery ticks until player is conscious
|
|
# Recovery rate is 0.1 per tick for both PL and stamina
|
|
# Need 1 tick to get above 0
|
|
await process_unconscious()
|
|
|
|
# After one tick, both should be above 0
|
|
assert player.pl > 0.0
|
|
assert player.stamina > 0.0
|
|
assert player.posture != "unconscious"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_come_to_message(clear_state, mock_writer):
|
|
"""Test that 'come to' message is sent on recovery."""
|
|
player = Player(
|
|
name="TestPlayer", x=0, y=0, pl=0.0, stamina=0.0, writer=mock_writer
|
|
)
|
|
players["TestPlayer"] = player
|
|
|
|
# Clear any initial messages
|
|
mock_writer.written.clear()
|
|
|
|
# Process recovery
|
|
await process_unconscious()
|
|
|
|
# Check for come to message
|
|
assert any("come to" in msg.lower() for msg in mock_writer.written)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_snap_neck_requires_unconscious(clear_state):
|
|
"""Test that snap neck only works on unconscious target."""
|
|
from mudlib.commands.snapneck import cmd_snap_neck
|
|
|
|
attacker_writer = MockWriter()
|
|
defender_writer = MockWriter()
|
|
|
|
attacker = Player(name="Attacker", x=0, y=0, writer=attacker_writer)
|
|
defender = Player(name="Defender", x=0, y=0, writer=defender_writer, pl=100.0)
|
|
|
|
players["Attacker"] = attacker
|
|
players["Defender"] = defender
|
|
|
|
# Start encounter first
|
|
start_encounter(attacker, defender)
|
|
|
|
# Try to snap neck on conscious target
|
|
attacker_writer.written.clear()
|
|
await cmd_snap_neck(attacker, "Defender")
|
|
|
|
# Should fail with "not unconscious" message
|
|
assert any("unconscious" in msg.lower() for msg in attacker_writer.written)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_snap_neck_success(clear_state):
|
|
"""Test that snap neck works on unconscious target."""
|
|
from mudlib.commands.snapneck import cmd_snap_neck
|
|
|
|
attacker_writer = MockWriter()
|
|
defender_writer = MockWriter()
|
|
|
|
attacker = Player(name="Attacker", x=0, y=0, writer=attacker_writer)
|
|
defender = Player(name="Defender", x=0, y=0, writer=defender_writer, pl=0.0)
|
|
|
|
players["Attacker"] = attacker
|
|
players["Defender"] = defender
|
|
|
|
# Start encounter
|
|
encounter = start_encounter(attacker, defender)
|
|
|
|
# Clear messages
|
|
attacker_writer.written.clear()
|
|
defender_writer.written.clear()
|
|
|
|
# Snap neck on unconscious target
|
|
await cmd_snap_neck(attacker, "Defender")
|
|
|
|
# Should succeed with dramatic message
|
|
assert any("snap" in msg.lower() for msg in attacker_writer.written)
|
|
|
|
# Encounter should end
|
|
assert encounter not in active_encounters
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_snap_neck_message_sent_to_both(clear_state):
|
|
"""Test that snap neck message is sent to both parties."""
|
|
from mudlib.commands.snapneck import cmd_snap_neck
|
|
|
|
attacker_writer = MockWriter()
|
|
defender_writer = MockWriter()
|
|
|
|
attacker = Player(name="Attacker", x=0, y=0, writer=attacker_writer)
|
|
defender = Player(name="Defender", x=0, y=0, writer=defender_writer, pl=0.0)
|
|
|
|
players["Attacker"] = attacker
|
|
players["Defender"] = defender
|
|
|
|
# Start encounter
|
|
start_encounter(attacker, defender)
|
|
|
|
# Clear messages
|
|
attacker_writer.written.clear()
|
|
defender_writer.written.clear()
|
|
|
|
# Snap neck
|
|
await cmd_snap_neck(attacker, "Defender")
|
|
|
|
# Both should receive messages
|
|
assert len(attacker_writer.written) > 0
|
|
assert len(defender_writer.written) > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_knockout_ends_combat(clear_state, mock_writer):
|
|
"""Test that combat ends when a player is knocked out."""
|
|
from mudlib.combat.encounter import CombatEncounter, CombatState
|
|
|
|
attacker = Player(name="Attacker", x=0, y=0, writer=mock_writer)
|
|
defender = Player(name="Defender", x=0, y=0, writer=mock_writer, pl=1.0)
|
|
|
|
# Create a simple punch move for testing
|
|
move = CombatMove(
|
|
name="test punch",
|
|
command="testpunch",
|
|
move_type="attack",
|
|
damage_pct=0.5,
|
|
stamina_cost=10.0,
|
|
timing_window_ms=850,
|
|
countered_by=[],
|
|
)
|
|
|
|
encounter = CombatEncounter(attacker=attacker, defender=defender)
|
|
encounter.state = CombatState.RESOLVE
|
|
encounter.current_move = move
|
|
|
|
# Resolve should detect knockout
|
|
result = encounter.resolve()
|
|
|
|
# Combat should end
|
|
assert result.combat_ended
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_partial_recovery(clear_state, mock_writer):
|
|
"""Test that only PL or stamina being above 0 doesn't wake player."""
|
|
player = Player(
|
|
name="TestPlayer", x=0, y=0, pl=5.0, stamina=0.0, writer=mock_writer
|
|
)
|
|
players["TestPlayer"] = player
|
|
|
|
# Player should be unconscious (stamina is 0)
|
|
assert player.posture == "unconscious"
|
|
|
|
# Process recovery - only stamina should increase
|
|
await process_unconscious()
|
|
|
|
# Check that PL wasn't affected (started at 5.0, should stay at 5.0)
|
|
assert player.pl == 5.0
|
|
# Stamina should have increased
|
|
assert player.stamina > 0.0
|
|
|
|
# Player should now be conscious (both > 0)
|
|
assert player.posture != "unconscious"
|