Wire target resolution into combat commands

This commit is contained in:
Jared Miller 2026-02-14 01:16:03 -05:00
parent 3f042de360
commit 86797c3a82
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 147 additions and 14 deletions

View file

@ -9,7 +9,7 @@ from mudlib.combat.engine import get_encounter, start_encounter
from mudlib.combat.moves import CombatMove, load_moves from mudlib.combat.moves import CombatMove, load_moves
from mudlib.combat.stamina import check_stamina_cues from mudlib.combat.stamina import check_stamina_cues
from mudlib.commands import CommandDefinition, register from mudlib.commands import CommandDefinition, register
from mudlib.player import Player, players from mudlib.player import Player
from mudlib.render.colors import colorize from mudlib.render.colors import colorize
from mudlib.render.pov import render_pov from mudlib.render.pov import render_pov
@ -32,15 +32,16 @@ async def do_attack(player: Player, target_args: str, move: CombatMove) -> None:
target = None target = None
target_name = target_args.strip() target_name = target_args.strip()
if encounter is None and target_name: if encounter is None and target_name:
target = players.get(target_name) from mudlib.targeting import find_entity_on_tile
if target is None and player.location is not None:
from mudlib.mobs import get_nearby_mob
from mudlib.zone import Zone
if isinstance(player.location, Zone): target = find_entity_on_tile(target_name, player)
target = get_nearby_mob(
target_name, player.x, player.y, player.location # If no target found on same z-axis, check if one exists on different z-axis
) if target is None:
any_z_target = find_entity_on_tile(target_name, player, z_filter=False)
if any_z_target is not None:
await player.send("You can't reach them from here!\r\n")
return
# Check stamina # Check stamina
if player.stamina < move.stamina_cost: if player.stamina < move.stamina_cost:
@ -53,11 +54,6 @@ async def do_attack(player: Player, target_args: str, move: CombatMove) -> None:
await player.send("You need a target to start combat.\r\n") await player.send("You need a target to start combat.\r\n")
return return
# Check altitude match before starting combat
if getattr(player, "flying", False) != getattr(target, "flying", False):
await player.send("You can't reach them from here!\r\n")
return
# Start new encounter # Start new encounter
try: try:
encounter = start_encounter(player, target) encounter = start_encounter(player, target)

View file

@ -0,0 +1,137 @@
"""Tests for combat command targeting integration."""
import pytest
from mudlib.combat.commands import do_attack
from mudlib.combat.moves import CombatMove
from mudlib.mobs import Mob
from mudlib.player import Player
@pytest.fixture
def attack_move():
"""Create a test attack move."""
return CombatMove(
name="punch left",
command="punch",
variant="left",
move_type="attack",
stamina_cost=5,
timing_window_ms=850,
telegraph="telegraphs a left punch at {defender}",
telegraph_color="yellow",
aliases=[],
)
@pytest.mark.asyncio
async def test_attack_with_prefix_match(player, nearby_player, attack_move):
"""Attack with prefix match finds target."""
player.stamina = 10
# Use "veg" to find Vegeta
await do_attack(player, "veg", attack_move)
# Should send combat start message
sent = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "You engage Vegeta in combat!" in sent
assert "You use punch left!" in sent
@pytest.mark.asyncio
async def test_attack_with_ordinal(player, nearby_player, test_zone, attack_move):
"""Attack with ordinal selects correct target."""
# Create two more players on the same tile, both starting with "p"
from unittest.mock import AsyncMock, MagicMock
mock_writer2 = MagicMock()
mock_writer2.write = MagicMock()
mock_writer2.drain = AsyncMock()
mock_reader2 = MagicMock()
player2 = Player(name="Piccolo", x=0, y=0, reader=mock_reader2, writer=mock_writer2)
player2.location = test_zone
test_zone._contents.append(player2)
mock_writer3 = MagicMock()
mock_writer3.write = MagicMock()
mock_writer3.drain = AsyncMock()
mock_reader3 = MagicMock()
player3 = Player(name="Puar", x=0, y=0, reader=mock_reader3, writer=mock_writer3)
player3.location = test_zone
test_zone._contents.append(player3)
from mudlib.player import players
players[player2.name] = player2
players[player3.name] = player3
player.stamina = 10
# Use "2.p" to find the second player starting with "p" (should be Puar)
await do_attack(player, "2.p", attack_move)
# Should engage the second "p" match
sent = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert (
"You engage Piccolo in combat!" in sent or "You engage Puar in combat!" in sent
)
@pytest.mark.asyncio
async def test_attack_nonexistent_target(player, attack_move):
"""Attack on non-existent target shows error."""
player.stamina = 10
await do_attack(player, "nobody", attack_move)
sent = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "You need a target to start combat." in sent
@pytest.mark.asyncio
async def test_attack_z_axis_mismatch(player, nearby_player, attack_move):
"""Attack on different z-axis is blocked with specific error message."""
player.stamina = 10
player.flying = True
nearby_player.flying = False
# find_entity_on_tile returns None due to z-axis mismatch
await do_attack(player, "Vegeta", attack_move)
sent = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "You can't reach them from here!" in sent
@pytest.mark.asyncio
async def test_attack_mob_with_prefix(player, test_zone, attack_move):
"""Attack mob with prefix match."""
player.stamina = 10
# Create a mob on the same tile
mob = Mob(name="Goblin", x=0, y=0)
mob.location = test_zone
test_zone._contents.append(mob)
# Use "gob" to find the goblin
await do_attack(player, "gob", attack_move)
sent = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "You engage Goblin in combat!" in sent
@pytest.mark.asyncio
async def test_attack_skips_dead_mob(player, test_zone, attack_move):
"""Attack skips dead mobs."""
player.stamina = 10
# Create a dead mob on the same tile
mob = Mob(name="Goblin", x=0, y=0)
mob.location = test_zone
mob.alive = False
test_zone._contents.append(mob)
# Should not find the dead mob
await do_attack(player, "goblin", attack_move)
sent = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "You need a target to start combat." in sent