"""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!"