implementation patterns
Guide for implementation patterns
Implementation Patterns
Core Functions
start_quest
Initializes a new quest in the player's active quest list.
pub fn start_quest(
quest_id: &str,
active_quests: &mut ActiveQuests,
quest_db: &QuestDatabase,
) -> Result<(), String> {
// 1. Check if already active
if active_quests.active.iter().any(|q| q.quest_id == quest_id) {
return Err("Quest already active".to_string());
}
// 2. Verify quest exists
let quest = quest_db.quests.get(quest_id)
.ok_or("Quest not found in database")?;
// 3. Find initial step (step "0")
if !quest.steps.contains_key("0") {
return Err("Quest has no initial step".to_string());
}
// 4. Add to active quests
active_quests.active.push(QuestProgress {
quest_id: quest_id.to_string(),
current_step: "0".to_string(),
});
Ok(())
}Usage:
start_quest("the_lost_scroll", &mut active_quests, &quest_db)?;advance_quest_step
Moves quest to the next step in its progression.
pub fn advance_quest_step(
quest_id: &str,
active_quests: &mut ActiveQuests,
quest_db: &QuestDatabase,
) -> Result<bool, String> {
// 1. Find active quest
let progress = active_quests.active.iter_mut()
.find(|p| p.quest_id == quest_id)
.ok_or("Quest not active")?;
// 2. Get quest definition
let quest = quest_db.quests.get(quest_id)
.ok_or("Quest not found")?;
// 3. Get current step
let current_step = quest.steps.get(&progress.current_step)
.ok_or("Current step not found")?;
// 4. Check if quest is finished
if current_step.finish.unwrap_or(false) {
return Ok(true);
}
// 5. Get next step
let next_step_id = match current_step.next_step {
Some(id) => id.to_string(),
None => return Ok(true), // No next step = finished
};
// 6. Verify next step exists
if !quest.steps.contains_key(&next_step_id) {
return Err(format!("Next step {} not found", next_step_id));
}
// 7. Update progress
progress.current_step = next_step_id;
Ok(false) // Not finished yet
}Usage:
let finished = advance_quest_step("the_lost_scroll", &mut active_quests, &quest_db)?;
if finished {
complete_quest("the_lost_scroll", player_entity, &mut active_quests, &quest_db)?;
}complete_quest
Finalizes quest, grants rewards, moves to completed list.
pub fn complete_quest(
quest_id: &str,
player: Entity,
active_quests: &mut ActiveQuests,
quest_db: &QuestDatabase,
) -> Result<QuestRewards, String> {
// 1. Remove from active
let idx = active_quests.active.iter()
.position(|p| p.quest_id == quest_id)
.ok_or("Quest not active")?;
active_quests.active.remove(idx);
// 2. Add to completed
if !active_quests.completed.contains(&quest_id.to_string()) {
active_quests.completed.push(quest_id.to_string());
}
// 3. Get rewards
let quest = quest_db.quests.get(quest_id)
.ok_or("Quest not found")?;
let rewards = quest.rewards.clone()
.unwrap_or_default();
// 4. Grant rewards (caller's responsibility to send events)
Ok(rewards)
}Usage:
let rewards = complete_quest("the_lost_scroll", player_entity, &mut active_quests, &quest_db)?;
// Grant XP
if let Some(xp) = rewards.xp {
xp_writer.send(XpGainEvent {
entity: player_entity,
skill: SkillType::Quest, // Or specific skill
amount: xp as f32,
});
}
// Grant items
for item_reward in rewards.items.unwrap_or_default() {
add_item_writer.send(AddItemEvent {
entity: player_entity,
item: ItemId(string_to_id(&item_reward.item_id)),
quantity: item_reward.quantity,
});
}Quest Listener Patterns
Dialogue Listener
Tracks NPC interactions for dialogue steps.
pub fn track_quest_dialogue(
mut npc_events: EventReader<NPCInteractionEvent>,
mut active_quests: ResMut<ActiveQuests>,
quest_db: Res<QuestDatabase>,
mut step_events: EventWriter<StepCompletedEvent>,
mut quest_complete_events: EventWriter<QuestCompletedEvent>,
) {
for event in npc_events.read() {
// Clone to avoid borrow issues
let active_quest_ids: Vec<String> = active_quests.active.iter()
.map(|p| p.quest_id.clone())
.collect();
for quest_id in active_quest_ids {
let Some(quest) = quest_db.quests.get(&quest_id) else { continue };
// Get current progress
let Some(progress) = active_quests.active.iter()
.find(|p| p.quest_id == quest_id) else { continue };
let Some(step) = quest.steps.get(&progress.current_step) else { continue };
// Check if this step matches the interaction
if step.step_type.as_deref() == Some("dialogue")
&& step.target_npc.as_deref() == Some(&event.npc_id) {
// Advance quest
match advance_quest_step(&quest_id, &mut active_quests, &quest_db) {
Ok(finished) => {
step_events.send(StepCompletedEvent {
quest_id: quest_id.clone(),
step_id: progress.current_step.clone(),
player: event.player,
});
if finished {
quest_complete_events.send(QuestCompletedEvent {
quest_id: quest_id.clone(),
player: event.player,
});
}
},
Err(e) => warn!("Failed to advance quest {}: {}", quest_id, e),
}
break; // Only advance one quest per interaction
}
}
}
}Kill Listener
Tracks creature defeats for kill steps.
pub fn track_quest_kills(
mut kill_events: EventReader<EnemyDefeatedEvent>,
mut active_quests: ResMut<ActiveQuests>,
quest_db: Res<QuestDatabase>,
creatures: Query<&Creature>,
mut step_events: EventWriter<StepCompletedEvent>,
) {
for event in kill_events.read() {
// Get creature type
let Ok(creature) = creatures.get(event.enemy) else { continue };
for progress in &mut active_quests.active {
let Some(quest) = quest_db.quests.get(&progress.quest_id) else { continue };
let Some(step) = quest.steps.get(&progress.current_step) else { continue };
// Check if this is a kill step for this creature
if step.step_type.as_deref() == Some("kill")
&& step.target_creature.as_deref() == Some(&creature.creature_id) {
// Note: Kill count tracking would need additional state
// For now, simplified to single kill
let finished = advance_quest_step(&progress.quest_id, &mut active_quests, &quest_db);
step_events.send(StepCompletedEvent {
quest_id: progress.quest_id.clone(),
step_id: progress.current_step.clone(),
player: event.killer,
});
break;
}
}
}
}UI Integration Patterns
Quest HUD
Displays current quest objective.
pub fn render_quest_hud(
mut contexts: EguiContexts,
active_quests: Res<ActiveQuests>,
quest_db: Res<QuestDatabase>,
) {
let ctx = contexts.ctx_mut();
egui::Area::new("quest_hud")
.fixed_pos(egui::pos2(10.0, 10.0))
.show(ctx, |ui| {
ui.visuals_mut().window_fill = egui::Color32::from_black_alpha(200);
if let Some(progress) = active_quests.active.first() {
if let Some(quest) = quest_db.quests.get(&progress.quest_id) {
if let Some(step) = quest.steps.get(&progress.current_step) {
ui.heading(&quest.name);
ui.label(&step.description);
}
}
} else {
ui.label("No active quest");
}
});
}Quest Journal (Full UI)
pub fn render_quest_journal(
ui: &mut egui::Ui,
active_quests: &ActiveQuests,
quest_db: &QuestDatabase,
) {
ui.heading("Active Quests");
ui.separator();
for progress in &active_quests.active {
if let Some(quest) = quest_db.quests.get(&progress.quest_id) {
ui.group(|ui| {
ui.heading(&quest.name);
ui.label(&quest.description);
ui.separator();
// Show all steps with progress indicator
for (step_id, step) in &quest.steps {
let is_current = step_id == &progress.current_step;
let icon = if is_current { "▶" } else { "○" };
ui.horizontal(|ui| {
ui.label(icon);
ui.label(&step.description);
});
}
});
}
}
ui.separator();
ui.heading("Completed Quests");
ui.separator();
for quest_id in &active_quests.completed {
if let Some(quest) = quest_db.quests.get(quest_id) {
ui.label(format!("✓ {}", quest.name));
}
}
}Testing Patterns
Setup Helper
fn setup_test_db() -> QuestDatabase {
let mut db = QuestDatabase::default();
let mut steps = HashMap::new();
steps.insert("0".to_string(), QuestStep {
description: "Start".to_string(),
step_type: Some("dialogue".to_string()),
target_npc: Some("npc_1".to_string()),
next_step: Some(10),
});
steps.insert("10".to_string(), QuestStep {
description: "Middle".to_string(),
step_type: Some("interaction".to_string()),
target_npc: Some("obj_1".to_string()),
next_step: None,
});
db.quests.insert("test_quest".to_string(), QuestDefinition {
id: "test_quest".to_string(),
name: "Test Quest".to_string(),
description: "A test quest".to_string(),
category: None,
difficulty: None,
steps,
rewards: None,
});
db
}Test Patterns
#[test]
fn test_quest_lifecycle() {
let db = setup_test_db();
let mut active = ActiveQuests::default();
// Start
assert!(start_quest("test_quest", &mut active, &db).is_ok());
assert_eq!(active.active.len(), 1);
assert_eq!(active.active[0].current_step, "0");
// Advance
let finished = advance_quest_step("test_quest", &mut active, &db).unwrap();
assert!(!finished);
assert_eq!(active.active[0].current_step, "10");
// Complete
let finished = advance_quest_step("test_quest", &mut active, &db).unwrap();
assert!(finished);
let entity = Entity::PLACEHOLDER;
complete_quest("test_quest", entity, &mut active, &db).unwrap();
assert_eq!(active.active.len(), 0);
assert!(active.completed.contains(&"test_quest".to_string()));
}Common Pitfalls
1. Forgetting to Clone quest_id
// ❌ Wrong - borrow conflict
for progress in &active_quests.active {
advance_quest_step(&progress.quest_id, &mut active_quests, &quest_db);
}
// ✅ Correct - clone IDs first
let quest_ids: Vec<String> = active_quests.active.iter()
.map(|p| p.quest_id.clone())
.collect();
for quest_id in quest_ids {
advance_quest_step(&quest_id, &mut active_quests, &quest_db);
}2. Not Checking Step Existence
// ❌ Wrong - may panic
let step = quest.steps.get(&progress.current_step).unwrap();
// ✅ Correct - handle missing steps
let Some(step) = quest.steps.get(&progress.current_step) else {
warn!("Step {} not found for quest {}", progress.current_step, quest_id);
continue;
};3. Forgetting Terminal Step Check
// ❌ Wrong - infinite loop if next_step points to self
progress.current_step = current_step.next_step.unwrap().to_string();
// ✅ Correct - check for finish conditions
if current_step.finish.unwrap_or(false) || current_step.next_step.is_none() {
return Ok(true);
}