← All Workshops

MudEngine Part 9: The Twin Guardians

Step 6 / 11

Server Quest Logic

Now we add the core quest engine. We need two new functions in the srv module:

  1. update_quest — after any player move or disconnect, check if the quest should activate/deactivate, mutate room exits accordingly, broadcast events, and kick players from the vault if it closes
  2. is_quest_guardian_room — check if a given room ID is one of the guardian rooms

These functions run inside the GAME_STATE lock, so they have atomic access to all game state.

Add these functions inside the mod srv block in src/main.rs:

mud-engine/src/main.rs
    /// Check whether a room is a quest guardian pedestal.
    pub fn is_quest_guardian_room(state: &GameState, room_id: usize) -> bool {
        state
            .quest
            .as_ref()
            .map(|q| q.config.guardians.iter().any(|g| g.room_id == room_id))
            .unwrap_or(false)
    }

    /// Evaluate quest conditions and mutate game state accordingly.
    /// Returns the event message to broadcast, or None if no state change.
    pub fn update_quest(state: &mut GameState) -> Option<(bool, String)> {
        let quest = state.quest.as_mut()?;

        // Don't bother with pedestals if quest is already completed
        if quest.completed {
            return None;
        }

        // Collect which guardian rooms currently have at least one player
        let all_occupied = quest
            .config
            .guardians
            .iter()
            .all(|g| state.players.values().any(|p| p.room == g.room_id));

        let now_active = all_occupied && state.players.len() >= 2;

        if now_active == quest.previously_active {
            return None; // no change
        }

        quest.previously_active = now_active;

        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, format!(
                "☀️🌙 The Twin Guardians are united! A shimmering portal opens in {}!",
                state.rooms.iter()
                    .find(|r| r.id == reward.source_room_id)
                    .map(|r| &r.name[..])
                    .unwrap_or("the source room")
            )))
        } else {
            // Remove dynamic exit; kick any players in the target room back to source
            let reward = &quest.config.reward;
            let source_id = reward.source_room_id;
            if let Some(room) = state.rooms.iter_mut().find(|r| r.id == source_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");
            }

            // Teleport any player in the target room back to source
            let victims: Vec<String> = state
                .players
                .iter()
                .filter(|(_, p)| p.room == reward.target_room_id)
                .map(|(id, _)| id.clone())
                .collect();

            for victim_id in &victims {
                if let Some(player) = state.players.get_mut(victim_id) {
                    player.room = source_id;
                }
            }

            Some((false, format!(
                "🌙☀️ The Twin Guardians part ways! The portal to the {} seals shut!",
                reward.target_name
            )))
        }
    }

    /// Check whether the quest retrieval item has arrived in the completion room.
    /// If so, mark the quest completed and return a celebration message.
    pub fn check_quest_completion(state: &mut GameState) -> Option<String> {
        let quest = state.quest.as_mut()?;
        if quest.completed {
            return None;
        }

        let item = &quest.config.retrieval_item;
        let room_id = quest.config.completion_room;

        // Check if the item is on the floor of the completion room
        let on_floor = state
            .rooms
            .iter()
            .any(|r| r.id == room_id && r.items.contains(item));

        // Check if any player in the completion room has the item in inventory
        let in_inventory = state
            .players
            .values()
            .any(|p| p.room == room_id && p.items.contains(item));

        if on_floor || in_inventory {
            quest.completed = true;
            Some(format!(
                "👑 King Aldric beams with joy! "The Star Fragment is returned! The realm is saved! "All hail the heroes who reclaimed the {}!"",
                item
            ))
        } else {
            None
        }
    }
💡 How update_quest works

Two new functions power the quest system:

update_quest — pedestal logic

Evaluated on every player move and disconnect. If the quest is already completed, it returns None immediately (guardians can leave their posts once the fragment is safe).

  1. Check conditions — are all guardian rooms occupied by at least one player? Requires state.players.len() >= 2 so a single player can't activate both pedestals alone.
  2. Compare with previous state — if nothing changed, return None (no broadcast needed)
  3. Add or remove dynamic exits — mutate room.exits directly for both the portal and the return path from the vault
  4. Kick stranded players — if the quest deactivated, anyone in the vault is returned to Town Square
  5. Return event message — the caller broadcasts QuestEvent to all connected clients

check_quest_completion — retrieval logic

Called separately whenever an item changes hands (take, drop, move). Checks if the retrieval_item is in the completion_room — either on the floor or in a player's inventory. When it is, marks the quest completed and returns a celebration message that gets broadcast as QuestCompleted.

Why two separate functions?

update_quest is about the gate (pedestals → portal). check_quest_completion is about the goal (fragment → King). They are evaluated at different points in the code: update_quest after every move/disconnect, check_quest_completion after every take, drop, and move. This separation keeps each function simple and focused on one concern.

Step 6 / 11