Show unlock requirements in help for locked moves
help <move> displays lock status and what's needed to unlock when the move has an unlock_condition the player hasn't met.
This commit is contained in:
parent
8e9e6f8245
commit
14dc2424ef
2 changed files with 180 additions and 0 deletions
|
|
@ -86,6 +86,20 @@ async def _show_single_command(
|
||||||
|
|
||||||
lines = [defn.name]
|
lines = [defn.name]
|
||||||
|
|
||||||
|
# Check if move is locked
|
||||||
|
if move is not None and move.unlock_condition:
|
||||||
|
base = move.command or move.name
|
||||||
|
if base not in player.unlocked_moves:
|
||||||
|
cond = move.unlock_condition
|
||||||
|
if cond.type == "kill_count":
|
||||||
|
lock_msg = f"[LOCKED] Defeat {cond.threshold} enemies to unlock."
|
||||||
|
elif cond.type == "mob_kills":
|
||||||
|
mob = cond.mob_name
|
||||||
|
lock_msg = f"[LOCKED] Defeat {cond.threshold} {mob}s to unlock."
|
||||||
|
else:
|
||||||
|
lock_msg = "[LOCKED]"
|
||||||
|
lines.append(f" {lock_msg}")
|
||||||
|
|
||||||
# Show description first for combat moves (most important context)
|
# Show description first for combat moves (most important context)
|
||||||
if move is not None and move.description:
|
if move is not None and move.description:
|
||||||
lines.append(f" {move.description}")
|
lines.append(f" {move.description}")
|
||||||
|
|
@ -139,6 +153,20 @@ async def _show_variant_overview(
|
||||||
|
|
||||||
lines = [defn.name]
|
lines = [defn.name]
|
||||||
|
|
||||||
|
# Check if any variant is locked
|
||||||
|
if variants and variants[0].unlock_condition:
|
||||||
|
base = variants[0].command or variants[0].name.split()[0]
|
||||||
|
if base not in player.unlocked_moves:
|
||||||
|
cond = variants[0].unlock_condition
|
||||||
|
if cond.type == "kill_count":
|
||||||
|
lock_msg = f"[LOCKED] Defeat {cond.threshold} enemies to unlock."
|
||||||
|
elif cond.type == "mob_kills":
|
||||||
|
mob = cond.mob_name
|
||||||
|
lock_msg = f"[LOCKED] Defeat {cond.threshold} {mob}s to unlock."
|
||||||
|
else:
|
||||||
|
lock_msg = "[LOCKED]"
|
||||||
|
lines.append(f" {lock_msg}")
|
||||||
|
|
||||||
# Show description from first variant (they all share the same one)
|
# Show description from first variant (they all share the same one)
|
||||||
if variants and variants[0].description:
|
if variants and variants[0].description:
|
||||||
lines.append(f" {variants[0].description}")
|
lines.append(f" {variants[0].description}")
|
||||||
|
|
|
||||||
152
tests/test_help_unlock.py
Normal file
152
tests/test_help_unlock.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
"""Tests for help command showing unlock status for combat moves."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mudlib.combat.moves import CombatMove, UnlockCondition
|
||||||
|
from mudlib.commands import dispatch, help # noqa: F401
|
||||||
|
from mudlib.player import Player, players
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clear_state():
|
||||||
|
"""Clear players before and after each test."""
|
||||||
|
players.clear()
|
||||||
|
yield
|
||||||
|
players.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_writer():
|
||||||
|
writer = MagicMock()
|
||||||
|
writer.write = MagicMock()
|
||||||
|
writer.drain = AsyncMock()
|
||||||
|
return writer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def player(mock_writer):
|
||||||
|
return Player(name="Test", writer=mock_writer)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_move_kill_count():
|
||||||
|
"""Mock move with kill_count unlock condition."""
|
||||||
|
return CombatMove(
|
||||||
|
name="roundhouse",
|
||||||
|
move_type="attack",
|
||||||
|
stamina_cost=30.0,
|
||||||
|
timing_window_ms=850,
|
||||||
|
aliases=["rh"],
|
||||||
|
description="A powerful spinning kick",
|
||||||
|
damage_pct=0.35,
|
||||||
|
unlock_condition=UnlockCondition(type="kill_count", threshold=5),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_move_mob_kills():
|
||||||
|
"""Mock move with mob_kills unlock condition."""
|
||||||
|
return CombatMove(
|
||||||
|
name="goblin slayer",
|
||||||
|
move_type="attack",
|
||||||
|
stamina_cost=25.0,
|
||||||
|
timing_window_ms=800,
|
||||||
|
description="Specialized technique against goblins",
|
||||||
|
damage_pct=0.40,
|
||||||
|
unlock_condition=UnlockCondition(
|
||||||
|
type="mob_kills", threshold=3, mob_name="goblin"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_move_no_unlock():
|
||||||
|
"""Mock move without unlock condition."""
|
||||||
|
return CombatMove(
|
||||||
|
name="jab",
|
||||||
|
move_type="attack",
|
||||||
|
stamina_cost=10.0,
|
||||||
|
timing_window_ms=600,
|
||||||
|
description="A quick straight punch",
|
||||||
|
damage_pct=0.15,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_locked_move_shows_kill_count_requirement(
|
||||||
|
player, mock_move_kill_count
|
||||||
|
):
|
||||||
|
"""Help for locked move shows kill count requirement."""
|
||||||
|
player.unlocked_moves = set()
|
||||||
|
|
||||||
|
moves = {"roundhouse": mock_move_kill_count, "rh": mock_move_kill_count}
|
||||||
|
with patch("mudlib.combat.commands.combat_moves", moves):
|
||||||
|
await dispatch(player, "help roundhouse")
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
|
||||||
|
assert "LOCKED" in output or "Locked" in output
|
||||||
|
assert "5" in output
|
||||||
|
assert "enemies" in output.lower() or "kills" in output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_locked_move_shows_mob_kills_requirement(
|
||||||
|
player, mock_move_mob_kills
|
||||||
|
):
|
||||||
|
"""Help for locked move shows mob-specific kill requirement."""
|
||||||
|
player.unlocked_moves = set()
|
||||||
|
|
||||||
|
moves = {"goblin slayer": mock_move_mob_kills}
|
||||||
|
with patch("mudlib.combat.commands.combat_moves", moves):
|
||||||
|
await dispatch(player, "help goblin slayer")
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
|
||||||
|
assert "LOCKED" in output or "Locked" in output
|
||||||
|
assert "3" in output
|
||||||
|
assert "goblin" in output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_unlocked_move_no_lock_notice(player, mock_move_kill_count):
|
||||||
|
"""Help for unlocked move shows normal help without lock notice."""
|
||||||
|
# Add move to unlocked_moves
|
||||||
|
player.unlocked_moves = {"roundhouse"}
|
||||||
|
|
||||||
|
moves = {"roundhouse": mock_move_kill_count, "rh": mock_move_kill_count}
|
||||||
|
with patch("mudlib.combat.commands.combat_moves", moves):
|
||||||
|
await dispatch(player, "help roundhouse")
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
|
||||||
|
assert "LOCKED" not in output and "Locked" not in output
|
||||||
|
assert "roundhouse" in output
|
||||||
|
assert "A powerful spinning kick" in output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_move_without_unlock_condition(player, mock_move_no_unlock):
|
||||||
|
"""Help for move without unlock condition shows normal help."""
|
||||||
|
player.unlocked_moves = set()
|
||||||
|
|
||||||
|
with patch("mudlib.combat.commands.combat_moves", {"jab": mock_move_no_unlock}):
|
||||||
|
await dispatch(player, "help jab")
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
|
||||||
|
assert "LOCKED" not in output and "Locked" not in output
|
||||||
|
assert "jab" in output
|
||||||
|
assert "A quick straight punch" in output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_locked_move_via_alias(player, mock_move_kill_count):
|
||||||
|
"""Help via alias for locked move shows unlock requirement."""
|
||||||
|
player.unlocked_moves = set()
|
||||||
|
|
||||||
|
moves = {"roundhouse": mock_move_kill_count, "rh": mock_move_kill_count}
|
||||||
|
with patch("mudlib.combat.commands.combat_moves", moves):
|
||||||
|
await dispatch(player, "help rh")
|
||||||
|
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||||
|
|
||||||
|
assert "LOCKED" in output or "Locked" in output
|
||||||
|
assert "5" in output
|
||||||
Loading…
Reference in a new issue