← All Workshops

MudEngine Part 6: Multiplayer

Step 13 / 18

Enhanced WorldGrid

WorldGrid is rewritten to read from reactive multiplayer signals instead of the single-player Signal<World>. It takes two ReadSignal props plus an onmove callback:

  • players — a HashMap<String, PlayerInfo> keyed by player ID
  • my_id — the current player's ID, used to determine which room is "active"
  • onmove: Callback<String> — fires with a direction name whenever the player moves (via D-pad, a room click, or the keyboard)

The grid is built by mapping over the static ROOM_NAMES array. For each room, WorldGrid:

  1. Looks up the current player's room via my_id to mark the active cell
  2. Uses the shared EXITS table to compute which rooms are reachable from the current room — those cells get the clickable prop, so they are highlighted and can be clicked to move
  3. Collects the names of every player whose room index matches, excluding the current player (so the indicator shows others, not yourself)
  4. Creates a RoomCell with those occupants, wired up so clicking a reachable cell calls onmove with that room's direction

Replace the WorldGrid in src/components/world_grid.rs with this multiplayer-aware version. It now imports the room data and message types from the game module, plus the RoomCell component:

mud-engine/src/components/world_grid.rs
use std::collections::HashMap;

use crate::components::room_cell::RoomCell;
use crate::game::messages::PlayerInfo;
use crate::game::room_data::{EXITS, ROOM_NAMES};
use dioxus::prelude::*;

#[component]
pub fn WorldGrid(
    players: ReadSignal<HashMap<String, PlayerInfo>>,
    my_id: ReadSignal<String>,
    onmove: Callback<String>,
) -> Element {
    let my_room = players.read().get(&*my_id.read()).map(|p| p.room);

    // Rooms reachable from the current room, mapped to the direction name
    let reachable: HashMap<usize, &str> = match my_room {
        Some(room) => EXITS
            .get(room)
            .map(|exits| {
                exits.iter().map(|&(dir, target)| (target, dir)).collect()
            })
            .unwrap_or_default(),
        None => HashMap::new(),
    };

    let cells: Vec<Element> = ROOM_NAMES.iter().enumerate().map(|(idx, name)| {
        let occupants: Vec<String> = players.read().values()
            .filter(|p| p.room == idx)
            .filter(|p| *my_id.read() != p.id)
            .map(|p| p.name.clone())
            .collect();
        let clickable = reachable.contains_key(&idx);
        let dir = reachable.get(&idx).copied().unwrap_or("").to_string();
        rsx! {
            RoomCell {
                key: "{idx}",
                name: name.clone(),
                active: my_room == Some(idx),
                clickable,
                players: occupants,
                onclick: move |_| if !dir.is_empty() { onmove.call(dir.clone()) },
            }
        }
    }).collect();

    rsx! {
        div { class: "world-grid", {cells.into_iter()} }
    }
}
🎯 How reachable rooms are computed

EXITS maps each room index to a list of (direction, destination) pairs. WorldGrid inverts that table for the current room: it builds a HashMap<usize, &str> where the key is a destination room index and the value is the direction string leading there.

For Town Square (room 4) the shared EXITS is:

&[("north", 1), ("south", 7), ("west", 3), ("east", 5)]

So reachable becomes {1: "north", 7: "south", 3: "west", 5: "east"}. Cells 1, 7, 3, and 5 are marked clickable and send "north", "south", "west", "east" respectively when clicked. Every other cell is inert — same behaviour as the Part 5 room cards, but now the move goes through the server.

If the player somehow has no room yet (my_room is None), reachable is empty and every cell is inert until State arrives.

Step 13 / 18