"""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 @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.WINDOW # 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!"