micro_quest 0.1.0

Structures and systems for managing game dialog & quests
Documentation
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum QuestState {
	InProgress,
	Success,
	Failure,
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QuestEntry {
	pub state: QuestState,
	pub description: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy", derive(bevy::reflect::TypeUuid))]
#[cfg_attr(feature = "bevy", uuid = "95fe3fd8-e3bc-11ed-a37a-03780a996b9a")]
pub struct Quest {
	pub id: QuestID,
	pub name: String,
	pub states: HashMap<StateID, QuestEntry>,
}

#[cfg(any(feature = "bevy"))]
use bevy::reflect::{ReflectDeserialize, ReflectSerialize};

/// A lightweight mapping of Quest ID to the quest-unique ID of one of that quest's states
///
/// The state ID should be used to seperately look up the details of that quest state in
/// whatever storage is used for quest information
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
	feature = "bevy",
	derive(
		bevy::prelude::Resource,
		bevy::reflect::Reflect,
		bevy::reflect::FromReflect
	)
)]
#[cfg_attr(feature = "bevy", reflect(Serialize, Deserialize))]
pub struct QuestLog(pub HashMap<QuestID, StateID>);
impl Deref for QuestLog {
	type Target = HashMap<QuestID, StateID>;
	fn deref(&self) -> &Self::Target {
		&self.0
	}
}
impl DerefMut for QuestLog {
	fn deref_mut(&mut self) -> &mut Self::Target {
		&mut self.0
	}
}

impl QuestLog {
	pub fn get_quest_state<'a>(
		&self,
		id: QuestID,
		locator: &'a impl QuestLocator,
	) -> Option<&'a QuestEntry> {
		locator
			.get_quest(&id)
			.zip(self.get(&id))
			.and_then(|(quest, state)| quest.states.get(state))
	}
}

pub type QuestID = String;
pub type StateID = String;

/// Provides functionality for other quest related structs to fetch quest information based on
/// a quest ID. Should be implemented for whatever storage mechanism is used to store quests
pub trait QuestLocator {
	fn get_quest(&self, id: impl ToString) -> Option<&Quest>;
}

#[cfg(all(test, any(feature = "bevy", feature = "serde")))]
mod parsing_test {
	use crate::quest_log::QuestLocator;
	use crate::{Quest, QuestState};

	#[derive(Debug, serde::Serialize, serde::Deserialize)]
	struct QuestFile {
		quests: Vec<Quest>,
	}

	impl QuestLocator for QuestFile {
		fn get_quest(&self, id: impl ToString) -> Option<&Quest> {
			self.quests.iter().find(|q| q.id == id.to_string())
		}
	}

	const RAW: &str = r#"
	[[quests]]
	id = "CRST_TOWN_GUARD_Q_001"
	name = "A Sound in the night..."
	[quests.states.10]
	state = "in_progress"
	description = "Ernesto the guard has given you some information about a strange noise heard in the depths of the night"
	[quests.states.20]
	state = "in_progress"
	description = "You kept watch in an alleyway, but unfortunately fell asleep. While asleep, the sound was heard once more. When you awoke, all you found was fur"
	[quests.states.30]
	state = "success"
	description = "You searched far and wide, and ended up finding out that it was a cat that was making that noise all along"
	[quests.states.40]
	state = "failure"
	description = "With the death of Count Mortant, and the dissolution of this realm, there is no longer any way to track down the source of the noise"
	"#;

	#[test]
	fn parses_simple_toml() {
		let value = toml::from_str(RAW);

		eprintln!("{:?}", value);

		assert!(value.is_ok());

		let list: QuestFile = value.unwrap();
		let value = list.quests.first().expect("Missing quest list quest");

		assert_eq!(value.id, "CRST_TOWN_GUARD_Q_001".to_string());
		assert_eq!(value.name, "A Sound in the night...".to_string());

		assert_eq!(value.states.len(), 4);
		assert!(value.states.contains_key(&"10".to_string()));
		assert!(value.states.contains_key(&"20".to_string()));
		assert!(value.states.contains_key(&"30".to_string()));
		assert!(value.states.contains_key(&"40".to_string()));

		assert_eq!(
			value.states[&"10".to_string()].state,
			QuestState::InProgress
		);
		assert_eq!(
			value.states[&"20".to_string()].state,
			QuestState::InProgress
		);
		assert_eq!(value.states[&"30".to_string()].state, QuestState::Success);
		assert_eq!(value.states[&"40".to_string()].state, QuestState::Failure);
	}
}