From d0c33911f3edfbe778d4e3cd05c07e4bd416d9fd Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sun, 8 Feb 2026 12:44:56 -0500 Subject: [PATCH] Wire edit command to open combat TOML files --- src/mudlib/combat/commands.py | 6 +- src/mudlib/commands/edit.py | 73 +++++++++++++++-- tests/test_editor_integration.py | 136 +++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 7 deletions(-) diff --git a/src/mudlib/combat/commands.py b/src/mudlib/combat/commands.py index 72735c6..e56dcc2 100644 --- a/src/mudlib/combat/commands.py +++ b/src/mudlib/combat/commands.py @@ -12,6 +12,7 @@ from mudlib.player import Player, players # Combat moves will be injected after loading combat_moves: dict[str, CombatMove] = {} +combat_content_dir: Path | None = None async def do_attack(player: Player, target_args: str, move: CombatMove) -> None: @@ -170,7 +171,10 @@ def register_combat_commands(content_dir: Path) -> None: Args: content_dir: Path to directory containing combat move TOML files """ - global combat_moves + global combat_moves, combat_content_dir + + # Save content directory for use by edit command + combat_content_dir = content_dir # Load all moves from content directory combat_moves = load_moves(content_dir) diff --git a/src/mudlib/commands/edit.py b/src/mudlib/commands/edit.py index 69336a3..e9bd2e9 100644 --- a/src/mudlib/commands/edit.py +++ b/src/mudlib/commands/edit.py @@ -1,5 +1,7 @@ """Edit command for entering the text editor.""" +from pathlib import Path + from mudlib.commands import CommandDefinition, register from mudlib.editor import Editor from mudlib.player import Player @@ -9,21 +11,80 @@ async def cmd_edit(player: Player, args: str) -> None: """Enter the text editor. Args: - player: The player executing the command - args: Command arguments (unused for now) + player: The player entering the editor + args: Optional argument - combat move name to edit """ + args = args.strip() - async def save_callback(content: str) -> None: - await player.send("Content saved.\r\n") + # Default: blank editor + initial_content = "" + content_type = "text" + save_callback_fn = _make_default_save_callback(player) + toml_path: Path | None = None + + # If args provided, try to load combat move TOML + if args: + from mudlib.combat.commands import combat_content_dir, combat_moves + + if combat_content_dir is None: + await player.send("Combat content not loaded.\r\n") + return + + # Look up the move - could be by name or alias + move = combat_moves.get(args) + + # If not found, try to find by command name (for variant bases) + if move is None: + for m in combat_moves.values(): + if m.command == args: + move = m + break + + if move is None: + await player.send(f"Unknown content: {args}\r\n") + return + + # Get the base command name to find the TOML file + toml_filename = f"{move.command}.toml" + toml_path = combat_content_dir / toml_filename + + if not toml_path.exists(): + await player.send(f"TOML file not found: {toml_filename}\r\n") + return + + # Read the file content + initial_content = toml_path.read_text() + content_type = "toml" + save_callback_fn = _make_toml_save_callback(player, toml_path) player.editor = Editor( - save_callback=save_callback, - content_type="text", + save_callback=save_callback_fn, + content_type=content_type, color_depth=player.color_depth, + initial_content=initial_content, ) player.mode_stack.append("editor") await player.send("Entering editor. Type :h for help.\r\n") +def _make_default_save_callback(player: Player): + """Create default save callback for blank editor.""" + + async def save_callback(content: str) -> None: + await player.send("Content saved.\r\n") + + return save_callback + + +def _make_toml_save_callback(player: Player, toml_path: Path): + """Create save callback for TOML file editing.""" + + async def save_callback(content: str) -> None: + toml_path.write_text(content) + await player.send(f"Saved {toml_path.name}\r\n") + + return save_callback + + # Register the edit command register(CommandDefinition("edit", cmd_edit, mode="normal")) diff --git a/tests/test_editor_integration.py b/tests/test_editor_integration.py index 8a47fed..c46c275 100644 --- a/tests/test_editor_integration.py +++ b/tests/test_editor_integration.py @@ -165,3 +165,139 @@ async def test_editor_prompt_uses_cursor(player): # This test verifies the cursor field exists and can be used for prompts assert player.editor.cursor == 1 # Shell loop prompt: f" {player.editor.cursor + 1}> " = " 2> " + + +@pytest.mark.asyncio +async def test_edit_no_args_opens_blank_editor(player): + """Test that edit with no args opens a blank editor.""" + await cmd_edit(player, "") + + assert player.editor is not None + assert player.editor.buffer == [] + assert player.mode == "editor" + + +@pytest.mark.asyncio +async def test_edit_combat_move_opens_toml(player, tmp_path): + """Test that edit roundhouse opens the TOML file for editing.""" + from mudlib.combat import commands as combat_commands + + # Create a test TOML file + toml_content = """name = "roundhouse" +aliases = ["rh"] +move_type = "attack" +stamina_cost = 8.0 +timing_window_ms = 2000 +""" + toml_file = tmp_path / "roundhouse.toml" + toml_file.write_text(toml_content) + + # Set up combat moves + combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")} + combat_commands.combat_content_dir = tmp_path + + await cmd_edit(player, "roundhouse") + + assert player.editor is not None + assert player.mode == "editor" + assert toml_content in "\n".join(player.editor.buffer) + + +@pytest.mark.asyncio +async def test_edit_combat_move_saves_to_disk(player, tmp_path, mock_writer): + """Test that saving in editor writes back to the TOML file.""" + from mudlib.combat import commands as combat_commands + + # Create a test TOML file + original_content = """name = "roundhouse" +aliases = ["rh"] +move_type = "attack" +""" + toml_file = tmp_path / "roundhouse.toml" + toml_file.write_text(original_content) + + # Set up combat moves + combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")} + combat_commands.combat_content_dir = tmp_path + + await cmd_edit(player, "roundhouse") + mock_writer.reset_mock() + + # Modify the buffer + player.editor.buffer = [ + 'name = "roundhouse"', + 'aliases = ["rh"]', + 'move_type = "attack"', + "stamina_cost = 9.0", + ] + + # Save + await player.editor.handle_input(":w") + + # Check that file was written + saved_content = toml_file.read_text() + assert "stamina_cost = 9.0" in saved_content + assert mock_writer.write.called + + +@pytest.mark.asyncio +async def test_edit_variant_base_opens_toml(player, tmp_path): + """Test that edit punch opens punch.toml (variant base name).""" + from mudlib.combat import commands as combat_commands + + # Create punch.toml with variants + toml_content = """name = "punch" +move_type = "attack" + +[variants.left] +aliases = ["pl"] +""" + toml_file = tmp_path / "punch.toml" + toml_file.write_text(toml_content) + + # Set up combat moves with variant + combat_commands.combat_moves = { + "punch left": MagicMock(command="punch", variant="left") + } + combat_commands.combat_content_dir = tmp_path + + await cmd_edit(player, "punch") + + assert player.editor is not None + assert player.mode == "editor" + assert "[variants.left]" in "\n".join(player.editor.buffer) + + +@pytest.mark.asyncio +async def test_edit_unknown_content_shows_error(player, mock_writer, tmp_path): + """Test that edit nonexistent shows an error.""" + from mudlib.combat import commands as combat_commands + + combat_commands.combat_moves = {} + combat_commands.combat_content_dir = tmp_path + + await cmd_edit(player, "nonexistent") + + assert player.editor is None + assert player.mode == "normal" + assert mock_writer.write.called + output = "".join([call[0][0] for call in mock_writer.write.call_args_list]) + assert "unknown" in output.lower() + assert "nonexistent" in output.lower() + + +@pytest.mark.asyncio +async def test_edit_combat_move_uses_toml_content_type(player, tmp_path): + """Test that editor for combat moves uses toml content type.""" + from mudlib.combat import commands as combat_commands + + toml_file = tmp_path / "roundhouse.toml" + toml_file.write_text("name = 'roundhouse'\n") + + combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")} + combat_commands.combat_content_dir = tmp_path + + await cmd_edit(player, "roundhouse") + + assert player.editor is not None + assert player.editor.content_type == "toml"