pub mod journal;
pub mod tracker;
pub use journal::{
JournalError, JournalNote, JournalSummary, ObjectiveAdvanceResult, ObjectiveProgress,
QuestEvent, QuestJournal, QuestProgress,
};
pub use tracker::{
GameEventType, ObjectiveMapper, QuestTracker, RewardDistributor, TrackerSession, TrackerStats,
};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct QuestId(pub u32);
impl QuestId {
pub fn new(id: u32) -> Self { Self(id) }
pub fn raw(self) -> u32 { self.0 }
}
impl std::fmt::Display for QuestId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "QuestId({})", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectiveId(pub u32);
impl ObjectiveId {
pub fn new(id: u32) -> Self { Self(id) }
pub fn raw(self) -> u32 { self.0 }
}
impl std::fmt::Display for ObjectiveId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ObjectiveId({})", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RewardId(pub u32);
impl RewardId {
pub fn new(id: u32) -> Self { Self(id) }
pub fn raw(self) -> u32 { self.0 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuestState {
Inactive,
Available,
Active,
Completed,
Failed,
Abandoned,
}
impl QuestState {
pub fn is_terminal(self) -> bool {
matches!(self, QuestState::Completed | QuestState::Failed | QuestState::Abandoned)
}
pub fn label(self) -> &'static str {
match self {
QuestState::Inactive => "Inactive",
QuestState::Available => "Available",
QuestState::Active => "Active",
QuestState::Completed => "Completed",
QuestState::Failed => "Failed",
QuestState::Abandoned => "Abandoned",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ObjectiveState {
Inactive,
Active,
Completed,
Failed,
Optional,
}
impl ObjectiveState {
pub fn is_done(self) -> bool {
matches!(self, ObjectiveState::Completed | ObjectiveState::Failed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuestCategory {
Main,
Side,
Daily,
Weekly,
Bounty,
Exploration,
Crafting,
Combat,
Social,
}
impl QuestCategory {
pub fn label(self) -> &'static str {
match self {
QuestCategory::Main => "Main",
QuestCategory::Side => "Side",
QuestCategory::Daily => "Daily",
QuestCategory::Weekly => "Weekly",
QuestCategory::Bounty => "Bounty",
QuestCategory::Exploration => "Exploration",
QuestCategory::Crafting => "Crafting",
QuestCategory::Combat => "Combat",
QuestCategory::Social => "Social",
}
}
pub fn is_time_limited(self) -> bool {
matches!(self, QuestCategory::Daily | QuestCategory::Weekly)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum QuestPriority {
Critical = 3,
High = 2,
Normal = 1,
Low = 0,
}
impl QuestPriority {
pub fn label(self) -> &'static str {
match self {
QuestPriority::Critical => "Critical",
QuestPriority::High => "High",
QuestPriority::Normal => "Normal",
QuestPriority::Low => "Low",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Reward {
pub experience: u32,
pub gold: u32,
pub items: Vec<(u32, u32)>,
pub reputation: Vec<(String, i32)>,
pub unlock_quests: Vec<QuestId>,
}
impl Reward {
pub fn new() -> Self { Self::default() }
pub fn with_experience(mut self, xp: u32) -> Self { self.experience = xp; self }
pub fn with_gold(mut self, gold: u32) -> Self { self.gold = gold; self }
pub fn with_item(mut self, item_id: u32, qty: u32) -> Self {
self.items.push((item_id, qty));
self
}
pub fn with_reputation(mut self, faction: impl Into<String>, delta: i32) -> Self {
self.reputation.push((faction.into(), delta));
self
}
pub fn unlock(mut self, quest_id: QuestId) -> Self {
self.unlock_quests.push(quest_id);
self
}
pub fn is_empty(&self) -> bool {
self.experience == 0
&& self.gold == 0
&& self.items.is_empty()
&& self.reputation.is_empty()
&& self.unlock_quests.is_empty()
}
pub fn merge(&mut self, other: &Reward) {
self.experience += other.experience;
self.gold += other.gold;
self.items.extend(other.items.iter().cloned());
self.reputation.extend(other.reputation.iter().cloned());
self.unlock_quests.extend(other.unlock_quests.iter().cloned());
}
}
#[derive(Debug, Clone)]
pub enum Prerequisite {
QuestComplete(QuestId),
QuestFailed(QuestId),
MinLevel(u32),
HasFlag(String),
NotFlag(String),
All(Vec<Prerequisite>),
Any(Vec<Prerequisite>),
}
impl Prerequisite {
pub fn check(&self, view: &PrerequisiteView) -> bool {
match self {
Prerequisite::QuestComplete(id) => view.quest_is_complete(*id),
Prerequisite::QuestFailed(id) => view.quest_is_failed(*id),
Prerequisite::MinLevel(lvl) => view.player_level >= *lvl,
Prerequisite::HasFlag(flag) => view.has_flag(flag),
Prerequisite::NotFlag(flag) => !view.has_flag(flag),
Prerequisite::All(list) => list.iter().all(|p| p.check(view)),
Prerequisite::Any(list) => list.iter().any(|p| p.check(view)),
}
}
}
#[derive(Debug, Clone)]
pub enum ObjectiveType {
Kill { enemy_type: String },
Collect { item_id: u32, count: u32 },
Reach { location: String },
Talk { npc_id: u32 },
Craft { item_id: u32 },
Survive { duration: f32 },
Escort { npc_id: u32, destination: String },
Protect { npc_id: u32, duration: f32 },
Custom { key: String },
}
impl ObjectiveType {
pub fn label(&self) -> &str {
match self {
ObjectiveType::Kill { .. } => "Kill",
ObjectiveType::Collect { .. } => "Collect",
ObjectiveType::Reach { .. } => "Reach",
ObjectiveType::Talk { .. } => "Talk",
ObjectiveType::Craft { .. } => "Craft",
ObjectiveType::Survive { .. } => "Survive",
ObjectiveType::Escort { .. } => "Escort",
ObjectiveType::Protect { .. } => "Protect",
ObjectiveType::Custom { .. } => "Custom",
}
}
}
#[derive(Debug, Clone)]
pub struct ObjectiveDef {
pub id: ObjectiveId,
pub description: String,
pub obj_type: ObjectiveType,
pub target_count: u32,
pub optional: bool,
pub hidden: bool,
}
impl ObjectiveDef {
pub fn new(
id: ObjectiveId,
description: impl Into<String>,
obj_type: ObjectiveType,
target_count: u32,
) -> Self {
Self {
id,
description: description.into(),
obj_type,
target_count,
optional: false,
hidden: false,
}
}
pub fn optional(mut self) -> Self { self.optional = true; self }
pub fn hidden(mut self) -> Self { self.hidden = true; self }
}
#[derive(Debug, Clone)]
pub struct QuestDef {
pub id: QuestId,
pub title: String,
pub description: String,
pub category: QuestCategory,
pub priority: QuestPriority,
pub prerequisites: Vec<Prerequisite>,
pub objectives: Vec<ObjectiveDef>,
pub reward: Reward,
pub time_limit: Option<f32>,
pub repeatable: bool,
pub hidden_until_available: bool,
pub tags: Vec<String>,
}
impl QuestDef {
pub fn new(
id: QuestId,
title: impl Into<String>,
description: impl Into<String>,
category: QuestCategory,
priority: QuestPriority,
) -> Self {
Self {
id,
title: title.into(),
description: description.into(),
category,
priority,
prerequisites: Vec::new(),
objectives: Vec::new(),
reward: Reward::default(),
time_limit: None,
repeatable: false,
hidden_until_available: false,
tags: Vec::new(),
}
}
pub fn with_prerequisite(mut self, p: Prerequisite) -> Self {
self.prerequisites.push(p);
self
}
pub fn with_objective(mut self, obj: ObjectiveDef) -> Self {
self.objectives.push(obj);
self
}
pub fn with_reward(mut self, reward: Reward) -> Self {
self.reward = reward;
self
}
pub fn with_time_limit(mut self, seconds: f32) -> Self {
self.time_limit = Some(seconds);
self
}
pub fn repeatable(mut self) -> Self { self.repeatable = true; self }
pub fn hidden(mut self) -> Self { self.hidden_until_available = true; self }
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn required_objective_count(&self) -> usize {
self.objectives.iter().filter(|o| !o.optional).count()
}
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
}
#[derive(Debug, Clone)]
pub struct PrerequisiteView {
pub player_level: u32,
pub completed_quests: std::collections::HashSet<QuestId>,
pub failed_quests: std::collections::HashSet<QuestId>,
pub flags: std::collections::HashSet<String>,
}
impl PrerequisiteView {
pub fn new(
player_level: u32,
completed: std::collections::HashSet<QuestId>,
failed: std::collections::HashSet<QuestId>,
flags: std::collections::HashSet<String>,
) -> Self {
Self { player_level, completed_quests: completed, failed_quests: failed, flags }
}
pub fn quest_is_complete(&self, id: QuestId) -> bool {
self.completed_quests.contains(&id)
}
pub fn quest_is_failed(&self, id: QuestId) -> bool {
self.failed_quests.contains(&id)
}
pub fn has_flag(&self, flag: &str) -> bool {
self.flags.contains(flag)
}
}
#[derive(Debug, Default)]
pub struct QuestDatabase {
quests: HashMap<QuestId, QuestDef>,
}
impl QuestDatabase {
pub fn new() -> Self { Self::default() }
pub fn register(&mut self, def: QuestDef) -> bool {
if self.quests.contains_key(&def.id) { return false; }
self.quests.insert(def.id, def);
true
}
pub fn register_or_replace(&mut self, def: QuestDef) {
self.quests.insert(def.id, def);
}
pub fn get(&self, id: QuestId) -> Option<&QuestDef> {
self.quests.get(&id)
}
pub fn available_for_level(&self, level: u32) -> Vec<&QuestDef> {
self.quests
.values()
.filter(|def| {
def.prerequisites.iter().all(|p| match p {
Prerequisite::MinLevel(min) => level >= *min,
_ => true,
})
})
.collect()
}
pub fn get_by_category(&self, category: QuestCategory) -> Vec<&QuestDef> {
self.quests.values().filter(|d| d.category == category).collect()
}
pub fn get_by_tag(&self, tag: &str) -> Vec<&QuestDef> {
self.quests.values().filter(|d| d.has_tag(tag)).collect()
}
pub fn len(&self) -> usize { self.quests.len() }
pub fn is_empty(&self) -> bool { self.quests.is_empty() }
pub fn all(&self) -> impl Iterator<Item = &QuestDef> {
self.quests.values()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn make_db() -> QuestDatabase {
let mut db = QuestDatabase::new();
let def = QuestDef::new(
QuestId(1),
"The First Hunt",
"Slay 5 goblins.",
QuestCategory::Combat,
QuestPriority::Normal,
)
.with_objective(ObjectiveDef::new(
ObjectiveId(1),
"Kill goblins",
ObjectiveType::Kill { enemy_type: "goblin".into() },
5,
))
.with_reward(Reward::new().with_experience(100).with_gold(50));
db.register(def);
db
}
#[test]
fn register_and_retrieve() {
let db = make_db();
assert!(db.get(QuestId(1)).is_some());
assert!(db.get(QuestId(99)).is_none());
}
#[test]
fn duplicate_register_is_noop() {
let mut db = make_db();
let def2 = QuestDef::new(
QuestId(1),
"Duplicate",
"Should not overwrite",
QuestCategory::Side,
QuestPriority::Low,
);
assert!(!db.register(def2));
assert_eq!(db.get(QuestId(1)).unwrap().title, "The First Hunt");
}
#[test]
fn available_for_level_filter() {
let mut db = QuestDatabase::new();
let gated = QuestDef::new(
QuestId(2),
"Advanced Quest",
"Requires level 10",
QuestCategory::Main,
QuestPriority::High,
)
.with_prerequisite(Prerequisite::MinLevel(10));
db.register(gated);
assert!(db.available_for_level(5).is_empty());
assert_eq!(db.available_for_level(10).len(), 1);
}
#[test]
fn prerequisite_flag_check() {
let progress = PrerequisiteView::new(
1,
HashSet::new(),
HashSet::new(),
["found_cave".to_string()].iter().cloned().collect(),
);
assert!(Prerequisite::HasFlag("found_cave".into()).check(&progress));
assert!(!Prerequisite::HasFlag("missing_flag".into()).check(&progress));
assert!(Prerequisite::NotFlag("missing_flag".into()).check(&progress));
}
#[test]
fn reward_builder_chain() {
let r = Reward::new()
.with_experience(500)
.with_gold(200)
.with_item(42, 3)
.with_reputation("Ironguard", 10)
.unlock(QuestId(5));
assert_eq!(r.experience, 500);
assert_eq!(r.gold, 200);
assert_eq!(r.items.len(), 1);
assert_eq!(r.reputation.len(), 1);
assert_eq!(r.unlock_quests.len(), 1);
}
#[test]
fn reward_merge() {
let mut a = Reward::new().with_experience(100).with_gold(50);
let b = Reward::new().with_experience(200).with_gold(75).with_item(1, 2);
a.merge(&b);
assert_eq!(a.experience, 300);
assert_eq!(a.gold, 125);
assert_eq!(a.items.len(), 1);
}
#[test]
fn quest_category_label() {
assert_eq!(QuestCategory::Daily.label(), "Daily");
assert!(QuestCategory::Daily.is_time_limited());
assert!(!QuestCategory::Combat.is_time_limited());
}
#[test]
fn quest_state_terminal() {
assert!(QuestState::Completed.is_terminal());
assert!(QuestState::Failed.is_terminal());
assert!(QuestState::Abandoned.is_terminal());
assert!(!QuestState::Active.is_terminal());
}
#[test]
fn prerequisite_all_any() {
let progress = PrerequisiteView::new(
5,
[QuestId(1)].iter().cloned().collect(),
HashSet::new(),
HashSet::new(),
);
let all_ok = Prerequisite::All(vec![
Prerequisite::QuestComplete(QuestId(1)),
Prerequisite::MinLevel(5),
]);
assert!(all_ok.check(&progress));
let all_fail = Prerequisite::All(vec![
Prerequisite::QuestComplete(QuestId(1)),
Prerequisite::MinLevel(10),
]);
assert!(!all_fail.check(&progress));
let any_ok = Prerequisite::Any(vec![
Prerequisite::MinLevel(10),
Prerequisite::QuestComplete(QuestId(1)),
]);
assert!(any_ok.check(&progress));
}
}