224 lines
6.4 KiB
Python
224 lines
6.4 KiB
Python
"""Tests for z-axis altitude checks in combat."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from mudlib.combat import commands as combat_commands
|
|
from mudlib.combat.encounter import CombatState
|
|
from mudlib.combat.engine import get_encounter, start_encounter
|
|
from mudlib.combat.moves import load_moves
|
|
|
|
|
|
@pytest.fixture
|
|
def target(mock_reader, mock_writer, test_zone):
|
|
"""Alias for nearby_player fixture with name 'target' for this module."""
|
|
from mudlib.player import Player, players
|
|
|
|
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.PENDING
|
|
|
|
# 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.PENDING
|
|
|
|
# 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.PENDING
|
|
|
|
# Attacker flies during window
|
|
player.flying = True
|
|
|
|
# Resolve
|
|
result = encounter.resolve()
|
|
|
|
# Attack should miss
|
|
assert result.countered is True or result.damage == 0.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flying_dodge_messages_correct_grammar(player, target, punch_right):
|
|
"""Test flying dodge produces grammatically correct messages from both POVs."""
|
|
from mudlib.render.pov import render_pov
|
|
|
|
# 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.PENDING
|
|
|
|
# Defender flies during window
|
|
target.flying = True
|
|
|
|
# Resolve
|
|
result = encounter.resolve()
|
|
|
|
# Render from both POVs
|
|
attacker_msg = render_pov(result.resolve_template, player, player, target)
|
|
defender_msg = render_pov(result.resolve_template, target, player, target)
|
|
|
|
# Attacker POV should say "You miss — Vegeta is out of reach!"
|
|
assert attacker_msg == "You miss — Vegeta is out of reach!"
|
|
|
|
# Defender POV should say "Goku misses — You are out of reach!"
|
|
assert defender_msg == "Goku misses — You are out of reach!"
|