Wire target resolution into combat commands
This commit is contained in:
parent
3f042de360
commit
86797c3a82
2 changed files with 147 additions and 14 deletions
|
|
@ -9,7 +9,7 @@ from mudlib.combat.engine import get_encounter, start_encounter
|
|||
from mudlib.combat.moves import CombatMove, load_moves
|
||||
from mudlib.combat.stamina import check_stamina_cues
|
||||
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.pov import render_pov
|
||||
|
||||
|
|
@ -32,15 +32,16 @@ async def do_attack(player: Player, target_args: str, move: CombatMove) -> None:
|
|||
target = None
|
||||
target_name = target_args.strip()
|
||||
if encounter is None and target_name:
|
||||
target = players.get(target_name)
|
||||
if target is None and player.location is not None:
|
||||
from mudlib.mobs import get_nearby_mob
|
||||
from mudlib.zone import Zone
|
||||
from mudlib.targeting import find_entity_on_tile
|
||||
|
||||
if isinstance(player.location, Zone):
|
||||
target = get_nearby_mob(
|
||||
target_name, player.x, player.y, player.location
|
||||
)
|
||||
target = find_entity_on_tile(target_name, player)
|
||||
|
||||
# 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
|
||||
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")
|
||||
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
|
||||
try:
|
||||
encounter = start_encounter(player, target)
|
||||
|
|
|
|||
137
tests/test_combat_targeting.py
Normal file
137
tests/test_combat_targeting.py
Normal 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
|
||||
Loading…
Reference in a new issue