MudEngine Part 9: The Twin Guardians
Update the WebSocket Endpoint
The WebSocket handler needs several changes:
- After every successful move — call
update_quest()andcheck_quest_completion() - After Take/Drop — call
check_quest_completion()in case the star fragment was moved - On disconnect — call
update_quest()andcheck_quest_completion()because a guardian might have left or items dropped - Handle
QuestCompletedbroadcast alongsideQuestEvent
Server-side changes
Modify the movement handler to run both quest functions after every valid move:
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 } ); } } }
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.
// ── 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 });
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.
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"); } // ... }