← All Workshops

MudEngine Part 9: The Twin Guardians

Step 7 / 11

Update the WebSocket Endpoint

The WebSocket handler needs several changes:

  1. After every successful move — call update_quest() and check_quest_completion()
  2. After Take/Drop — call check_quest_completion() in case the star fragment was moved
  3. On disconnect — call update_quest() and check_quest_completion() because a guardian might have left or items dropped
  4. Handle QuestCompleted broadcast alongside QuestEvent

Server-side changes

Modify the movement handler to run both quest functions after every valid move:

mud-engine/src/main.rs
                        Ok(ClientMessage::Move { direction }) => {
                            let dir = match direction.as_str() {
                                "n" => "north", "s" => "south",
                                "e" => "east", "w" => "west",
                                "down" | "d" => "down",
                                "up" | "u" => "up",
                                d => d,
                            };

                            let (new_room, quest_event) = {
                                let mut state = srv::GAME_STATE.lock().unwrap();
                                let player = state.players.get_mut(&id).unwrap();
                                let current = player.room;

                                let next = if let Some(n) = srv::can_move(&state, current, dir) {
                                    player.room = n;
                                    srv::save_game_state(&state);
                                    Some(n)
                                } else if dir == "down" && current == 4 {
                                    // Direct vault descent check for Town Square
                                    if state.quest.as_ref().map(|q| q.previously_active).unwrap_or(false) {
                                        player.room = 9;
                                        srv::save_game_state(&state);
                                        Some(9)
                                    } else {
                                        None
                                    }
                                } else {
                                    None
                                };

                                let q_event = if next.is_some() {
                                    srv::update_quest(&mut state)
                                } else {
                                    None
                                };

                                (next, q_event)
                            };

                            if let Some(room) = new_room {
                                let _ = srv::BROADCAST.send(
                                    ServerMessage::PlayerMoved(PlayerInfo {
                                        id: id.clone(),
                                        name: player_name.clone(),
                                        room,
                                        items: Vec::new(),
                                    }),
                                );

                                if let Some((active, msg)) = quest_event {
                                    let _ = srv::BROADCAST.send(
                                        ServerMessage::QuestEvent { active, message: msg }
                                    );
                                }

                                // Check if the retrieval item arrived in the completion room
                                if let Some(celebration) =
                                    srv::check_quest_completion(&mut srv::GAME_STATE.lock().unwrap())
                                {
                                    let _ = srv::BROADCAST.send(
                                        ServerMessage::QuestCompleted { message: celebration }
                                    );
                                }
                            }
                        }
💡 Completion check in Take/Drop handlers

The check_quest_completion function must also be called after Take and Drop actions, since those are the moments the star fragment changes hands. Add this check inside both handlers, right after the successful action and broadcast:

// Inside Take handler, after the ItemTaken broadcast:
if let Some(celebration) =
    srv::check_quest_completion(&mut srv::GAME_STATE.lock().unwrap())
{
    let _ = srv::BROADCAST.send(
        ServerMessage::QuestCompleted { message: celebration }
    );
}

Same pattern in the Drop handler. This ensures that if a player drops the star fragment in Town Square — or takes it while standing there — the completion triggers immediately.

mud-engine/src/main.rs
        // ── Cleanup on disconnect ──
        {
            let mut state = srv::GAME_STATE.lock().unwrap();

            // Drop all carried items into the player's current room
            if let Some(player) = state.players.get(&id) {
                let room_id = player.room;
                if let Some(room) = state.rooms.iter_mut().find(|r| r.id == room_id) {
                    for item in &player.items {
                        if !room.items.contains(item) {
                            room.items.push(item.clone());
                        }
                    }
                }
            }

            state.players.remove(&id);

            // Re-evaluate quest — a guardian may have left
            if let Some((active, msg)) = srv::update_quest(&mut state) {
                let _ = srv::BROADCAST.send(
                    ServerMessage::QuestEvent { active, message: msg }
                );
            }

            srv::save_game_state(&state);
        }
        let _ = srv::BROADCAST.send(ServerMessage::PlayerLeft { id });
🎯 Why the direct vault descent check?

The movement handler has a special case for dir == "down" && current == 4:

} else if dir == "down" && current == 4 {
    if state.quest.as_ref().map(|q| q.previously_active).unwrap_or(false) {
        player.room = 9;
        ...
    }
}

This is needed because the dynamic exit is added to room 4's exits by update_quest, but update_quest runs after the move validation. On the very first frame where both pedestals are activated, the exit might not be added yet when the player tries to descend.

The direct check ensures that as long as the quest is active (previously_active == true), descending from Town Square always works. Once the exit is added to the room data, the normal can_move path also handles it — the direct check is a belt-and-suspenders approach.

For ascending from the vault back to Town Square, no special handling is needed — the quest activation adds a "up" exit in the opposite direction. Wait, let's fix that: we need to also add a return exit from room 9 to room 4.

mud-engine/src/main.rs
        if now_active {
            // Add dynamic exit from source room to target room
            let reward = &quest.config.reward;
            if let Some(room) = state.rooms.iter_mut().find(|r| r.id == reward.source_room_id) {
                if !room.exits.iter().any(|(d, _)| d == &reward.direction) {
                    room.exits.push((reward.direction.clone(), reward.target_room_id));
                }
            }
            // Also add the return exit so players can leave the vault
            if let Some(room) = state.rooms.iter_mut().find(|r| r.id == reward.target_room_id) {
                if !room.exits.iter().any(|(d, _)| d == "up") {
                    room.exits.push(("up".into(), reward.source_room_id));
                }
            }
            Some((true, ...))
        } else {
            // Remove both dynamic exits
            let reward = &quest.config.reward;
            if let Some(room) = state.rooms.iter_mut().find(|r| r.id == reward.source_room_id) {
                room.exits.retain(|(d, _)| d != &reward.direction);
            }
            if let Some(room) = state.rooms.iter_mut().find(|r| r.id == reward.target_room_id) {
                room.exits.retain(|(d, _)| d != "up");
            }
            // ...
        }
Step 7 / 11