Players must be at same altitude (both flying or both grounded) to initiate combat. Attack fails with 'You can't reach them from here!' if altitude differs.
239 lines
6.3 KiB
Python
239 lines
6.3 KiB
Python
"""Tests for z-axis altitude checks in combat."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.combat import commands as combat_commands
|
|
from mudlib.combat.encounter import CombatState
|
|
from mudlib.combat.engine import active_encounters, get_encounter, start_encounter
|
|
from mudlib.combat.moves import load_moves
|
|
from mudlib.player import Player, players
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_state():
|
|
"""Clear encounters and players before and after each test."""
|
|
active_encounters.clear()
|
|
players.clear()
|
|
yield
|
|
active_encounters.clear()
|
|
players.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_zone():
|
|
"""Create a test zone for players."""
|
|
terrain = [["." for _ in range(256)] for _ in range(256)]
|
|
zone = Zone(
|
|
name="testzone",
|
|
width=256,
|
|
height=256,
|
|
toroidal=True,
|
|
terrain=terrain,
|
|
impassable=set(),
|
|
)
|
|
return zone
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
return writer
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_reader():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_reader, mock_writer, test_zone):
|
|
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
|
|
p.location = test_zone
|
|
test_zone._contents.append(p)
|
|
players[p.name] = p
|
|
return p
|
|
|
|
|
|
@pytest.fixture
|
|
def target(mock_reader, mock_writer, test_zone):
|
|
t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
|
|
t.location = test_zone
|
|
test_zone._contents.append(t)
|
|
players[t.name] = t
|
|
return t
|
|
|
|
|
|
@pytest.fixture
|
|
def moves():
|
|
"""Load combat moves from content directory."""
|
|
content_dir = Path(__file__).parent.parent / "content" / "combat"
|
|
return load_moves(content_dir)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def inject_moves(moves):
|
|
"""Inject loaded moves into combat commands module."""
|
|
combat_commands.combat_moves = moves
|
|
yield
|
|
combat_commands.combat_moves = {}
|
|
|
|
|
|
@pytest.fixture
|
|
def punch_right(moves):
|
|
"""Get the punch right move."""
|
|
return moves["punch right"]
|
|
|
|
|
|
@pytest.fixture
|
|
def dodge_left(moves):
|
|
"""Get the dodge left move."""
|
|
return moves["dodge left"]
|
|
|
|
|
|
# --- Z-axis altitude check tests for starting combat ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attack_fails_when_attacker_flying_defender_grounded(
|
|
player, target, punch_right
|
|
):
|
|
"""Test attack fails when attacker is flying and defender is grounded."""
|
|
player.flying = True
|
|
target.flying = False
|
|
|
|
await combat_commands.do_attack(player, "Vegeta", punch_right)
|
|
|
|
# Combat should not start
|
|
encounter = get_encounter(player)
|
|
assert encounter is None
|
|
|
|
# Player should get error message
|
|
player.writer.write.assert_called()
|
|
messages = [call[0][0] for call in player.writer.write.call_args_list]
|
|
assert any("can't reach" in msg.lower() for msg in messages)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attack_fails_when_attacker_grounded_defender_flying(
|
|
player, target, punch_right
|
|
):
|
|
"""Test attack fails when attacker is grounded and defender is flying."""
|
|
player.flying = False
|
|
target.flying = True
|
|
|
|
await combat_commands.do_attack(player, "Vegeta", punch_right)
|
|
|
|
# Combat should not start
|
|
encounter = get_encounter(player)
|
|
assert encounter is None
|
|
|
|
# Player should get error message
|
|
player.writer.write.assert_called()
|
|
messages = [call[0][0] for call in player.writer.write.call_args_list]
|
|
assert any("can't reach" in msg.lower() for msg in messages)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attack_succeeds_when_both_flying(player, target, punch_right):
|
|
"""Test attack succeeds when both are flying."""
|
|
player.flying = True
|
|
target.flying = True
|
|
|
|
await combat_commands.do_attack(player, "Vegeta", punch_right)
|
|
|
|
# Combat should start
|
|
encounter = get_encounter(player)
|
|
assert encounter is not None
|
|
assert encounter.attacker is player
|
|
assert encounter.defender is target
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attack_succeeds_when_both_grounded(player, target, punch_right):
|
|
"""Test attack succeeds when both are grounded."""
|
|
player.flying = False
|
|
target.flying = False
|
|
|
|
await combat_commands.do_attack(player, "Vegeta", punch_right)
|
|
|
|
# Combat should start
|
|
encounter = get_encounter(player)
|
|
assert encounter is not None
|
|
assert encounter.attacker is player
|
|
assert encounter.defender is target
|
|
|
|
|
|
# --- Flying dodge tests (altitude mismatch at resolve time) ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flying_during_window_causes_miss(player, target, punch_right):
|
|
"""Test flying during window phase causes attack to miss at resolve time."""
|
|
# Both grounded, start combat
|
|
player.flying = False
|
|
target.flying = False
|
|
|
|
encounter = start_encounter(player, target)
|
|
encounter.attack(punch_right)
|
|
|
|
# Advance to WINDOW phase
|
|
encounter.state = CombatState.WINDOW
|
|
|
|
# Defender flies during window
|
|
target.flying = True
|
|
|
|
# Resolve
|
|
result = encounter.resolve()
|
|
|
|
# Attack should miss (countered or zero damage)
|
|
assert result.countered is True or result.damage == 0.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_both_flying_at_resolve_attack_lands(player, target, punch_right):
|
|
"""Test both flying at resolve time — attack lands normally."""
|
|
# Both flying, start combat
|
|
player.flying = True
|
|
target.flying = True
|
|
|
|
encounter = start_encounter(player, target)
|
|
encounter.attack(punch_right)
|
|
|
|
# Advance to WINDOW phase (no altitude change)
|
|
encounter.state = CombatState.WINDOW
|
|
|
|
# Resolve
|
|
result = encounter.resolve()
|
|
|
|
# Attack should land (damage > 0 unless defended)
|
|
# Since there's no defense, damage should be > 0
|
|
assert result.damage > 0.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attacker_flies_during_window_causes_miss(player, target, punch_right):
|
|
"""Test attacker flying during window phase causes attack to miss."""
|
|
# Both grounded, start combat
|
|
player.flying = False
|
|
target.flying = False
|
|
|
|
encounter = start_encounter(player, target)
|
|
encounter.attack(punch_right)
|
|
|
|
# Advance to WINDOW phase
|
|
encounter.state = CombatState.WINDOW
|
|
|
|
# Attacker flies during window
|
|
player.flying = True
|
|
|
|
# Resolve
|
|
result = encounter.resolve()
|
|
|
|
# Attack should miss
|
|
assert result.countered is True or result.damage == 0.0
|