Source code for pipeworks_mud_mapper.services.state.actions_room

"""Room-related zone state transitions.

These helpers implement the mutations for add/update/delete/undo
room actions. The logic mirrors the prior callback implementations,
while returning a structured ZoneTransition result for the state manager.
"""

from __future__ import annotations

from typing import Any

import dash_bootstrap_components as dbc
from dash import html

from pipeworks_mud_mapper.services.state.types import ZoneTransition


[docs] def add_room( *, zone_data: dict | None, room_id: str | None, room_name: str | None, room_description: str | None, coord_x: Any, coord_y: Any, coord_z: Any, ) -> ZoneTransition: """Add a new room to the zone. Returns a ZoneTransition with feedback when validation fails. """ if not zone_data: feedback = dbc.Alert( "No zone loaded. Create or select a zone first.", color="warning", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) room_id = (room_id or "").strip() if not room_id: feedback = dbc.Alert( "Room ID is required.", color="danger", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) if not room_id[0].isalpha() or not room_id.replace("_", "").isalnum(): feedback = dbc.Alert( "Room ID must start with a letter and contain only letters, numbers, and underscores.", color="danger", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) rooms = zone_data.get("rooms", {}) if room_id in rooms: feedback = dbc.Alert( f"Room '{room_id}' already exists.", color="warning", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) try: x = int(coord_x) if coord_x is not None else 0 y = int(coord_y) if coord_y is not None else 0 z = int(coord_z) if coord_z is not None else 0 except (ValueError, TypeError): feedback = dbc.Alert( "Coordinates must be integers.", color="danger", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) new_room = { "id": room_id, "name": (room_name or "").strip() or room_id, "description": (room_description or "").strip(), "coords": [x, y, z], "exits": {}, "items": [], } updated_zone = dict(zone_data) updated_zone["rooms"] = dict(zone_data.get("rooms", {})) updated_zone["rooms"][room_id] = new_room feedback = dbc.Alert( f"Room '{room_id}' added at ({x}, {y}, {z})", color="success", className="mb-0 py-2", duration=3000, ) return ZoneTransition( zone_data=updated_zone, feedback=feedback, unsaved=True, changed=True, )
[docs] def update_room( *, zone_data: dict | None, selected_room: str | None, room_name: str | None, room_description: str | None, coord_x: Any, coord_y: Any, coord_z: Any, ) -> ZoneTransition: """Update an existing room's properties.""" if not zone_data or not selected_room: feedback = dbc.Alert( "No room selected to update.", color="warning", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) rooms = zone_data.get("rooms", {}) if selected_room not in rooms: feedback = dbc.Alert( f"Room '{selected_room}' not found.", color="danger", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) try: x = int(coord_x) if coord_x is not None else 0 y = int(coord_y) if coord_y is not None else 0 z = int(coord_z) if coord_z is not None else 0 except (ValueError, TypeError): feedback = dbc.Alert( "Coordinates must be integers.", color="danger", className="mb-0 py-2", ) return ZoneTransition(zone_data=None, feedback=feedback, changed=False) updated_zone = dict(zone_data) updated_zone["rooms"] = dict(zone_data.get("rooms", {})) updated_room = dict(rooms[selected_room]) updated_room["name"] = (room_name or "").strip() or selected_room updated_room["description"] = (room_description or "").strip() updated_room["coords"] = [x, y, z] updated_zone["rooms"][selected_room] = updated_room feedback = dbc.Alert( f"Room '{selected_room}' updated.", color="success", className="mb-0 py-2", duration=3000, ) return ZoneTransition( zone_data=updated_zone, feedback=feedback, unsaved=True, changed=True, )
[docs] def delete_room( *, zone_data: dict | None, selected_room: str | None, ) -> ZoneTransition: """Delete a room and collect undo metadata.""" if not zone_data or not selected_room: return ZoneTransition(zone_data=None, changed=False) rooms = zone_data.get("rooms", {}) if selected_room not in rooms: return ZoneTransition(zone_data=None, changed=False) deleted_room = dict(rooms[selected_room]) removed_exits: list[dict[str, str]] = [] for room_id, other_room in rooms.items(): if room_id == selected_room: continue for direction, target in list(other_room.get("exits", {}).items()): if target == selected_room: removed_exits.append( { "room_id": room_id, "direction": direction, "target": selected_room, } ) undo_data = { "room_id": selected_room, "room_data": deleted_room, "removed_exits": removed_exits, } updated_zone = dict(zone_data) updated_zone["rooms"] = dict(zone_data.get("rooms", {})) del updated_zone["rooms"][selected_room] for exit_info in removed_exits: room_id = exit_info["room_id"] direction = exit_info["direction"] if room_id in updated_zone["rooms"]: updated_zone["rooms"][room_id] = dict(updated_zone["rooms"][room_id]) updated_zone["rooms"][room_id]["exits"] = dict( updated_zone["rooms"][room_id].get("exits", {}) ) if direction in updated_zone["rooms"][room_id]["exits"]: del updated_zone["rooms"][room_id]["exits"][direction] feedback = dbc.Alert( [ html.I(className="bi bi-trash me-2"), f"Room '{selected_room}' deleted.", ], color="warning", className="mb-0 py-2", duration=5000, ) return ZoneTransition( zone_data=updated_zone, feedback=feedback, unsaved=True, effects={"undo_data": undo_data}, changed=True, )
[docs] def undo_delete( *, zone_data: dict | None, undo_data: dict | None, ) -> ZoneTransition: """Restore a deleted room from undo metadata.""" if not zone_data or not undo_data: return ZoneTransition(zone_data=None, changed=False) room_id = undo_data.get("room_id") room_data = undo_data.get("room_data") removed_exits = undo_data.get("removed_exits", []) if not room_id or not room_data: return ZoneTransition(zone_data=None, changed=False) updated_zone = dict(zone_data) updated_zone["rooms"] = dict(zone_data.get("rooms", {})) updated_zone["rooms"][room_id] = room_data for exit_info in removed_exits: other_room_id = exit_info["room_id"] direction = exit_info["direction"] target = exit_info["target"] if other_room_id in updated_zone["rooms"]: updated_zone["rooms"][other_room_id] = dict(updated_zone["rooms"][other_room_id]) updated_zone["rooms"][other_room_id]["exits"] = dict( updated_zone["rooms"][other_room_id].get("exits", {}) ) updated_zone["rooms"][other_room_id]["exits"][direction] = target feedback = dbc.Alert( [ html.I(className="bi bi-arrow-counterclockwise me-2"), f"Room '{room_id}' restored.", ], color="success", className="mb-0 py-2", duration=3000, ) return ZoneTransition( zone_data=updated_zone, feedback=feedback, effects={"cleared_undo": True}, changed=True, )