MudEngine Part 9: The Twin Guardians
Server Quest Logic
Now we add the core quest engine. We need two new functions in the srv module:
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 closesis_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:
/// 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 } }
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).
- Check conditions — are all guardian rooms occupied by at least one player? Requires
state.players.len() >= 2so a single player can't activate both pedestals alone. - Compare with previous state — if nothing changed, return
None(no broadcast needed) - Add or remove dynamic exits — mutate
room.exitsdirectly for both the portal and the return path from the vault - Kick stranded players — if the quest deactivated, anyone in the vault is returned to Town Square
- Return event message — the caller broadcasts
QuestEventto 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.