use std::fmt;
use std::fs;
use std::io::BufReader;
use std::error;
use serde::{Serialize, Deserialize};
use serde_json::{from_reader, to_writer, to_writer_pretty};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(private_bounds)]
pub enum SpellField<T: fmt::Display>
{
Controlled(T),
Custom(String)
}
impl<T: fmt::Display> fmt::Display for SpellField<T>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
match self
{
Self::Controlled(controlled_value) => write!(f, "{}", controlled_value),
Self::Custom(custom_value) => write!(f, "{}", custom_value)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Level
{
Cantrip,
Level1,
Level2,
Level3,
Level4,
Level5,
Level6,
Level7,
Level8,
Level9
}
impl fmt::Display for Level
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
let level_str = "Level ";
let text = match self
{
Self::Cantrip => String::from("Cantrip"),
_ => format!("{}{}", level_str, u8::from(self))
};
write!(f, "{}", text)
}
}
impl TryFrom<u8> for Level
{
type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error>
{
match value
{
0 => Ok(Self::Cantrip),
1 => Ok(Self::Level1),
2 => Ok(Self::Level2),
3 => Ok(Self::Level3),
4 => Ok(Self::Level4),
5 => Ok(Self::Level5),
6 => Ok(Self::Level6),
7 => Ok(Self::Level7),
8 => Ok(Self::Level8),
9 => Ok(Self::Level9),
_ => Err("Spell levels must be between 0 and 9 (inclusive).")
}
}
}
impl From<&Level> for u8
{
fn from(level: &Level) -> Self
{
match level
{
Level::Cantrip => 0,
Level::Level1 => 1,
Level::Level2 => 2,
Level::Level3 => 3,
Level::Level4 => 4,
Level::Level5 => 5,
Level::Level6 => 6,
Level::Level7 => 7,
Level::Level8 => 8,
Level::Level9 => 9
}
}
}
impl From<Level> for u8
{
fn from(level: Level) -> Self
{
u8::from(&level)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum MagicSchool
{
Abjuration,
Conjuration,
Divination,
Enchantment,
Evocation,
Illusion,
Necromancy,
Transmutation
}
impl TryFrom<&str> for MagicSchool
{
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error>
{
match value.to_lowercase().as_str()
{
"abjuration" => Ok(Self::Abjuration),
"conjuration" => Ok(Self::Conjuration),
"divination" => Ok(Self::Divination),
"enchantment" => Ok(Self::Enchantment),
"evocation" => Ok(Self::Evocation),
"illusion" => Ok(Self::Illusion),
"necromancy" => Ok(Self::Necromancy),
"transmutation" => Ok(Self::Transmutation),
_ => Err("Invalid MagicSchool string.")
}
}
}
impl fmt::Display for MagicSchool
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
let text = match self
{
Self::Abjuration => String::from("Abjuration"),
Self::Conjuration => String::from("Conjuration"),
Self::Divination => String::from("Divination"),
Self::Enchantment => String::from("Enchantment"),
Self::Evocation => String::from("Evocation"),
Self::Illusion => String::from("Illusion"),
Self::Necromancy => String::from("Necromancy"),
Self::Transmutation => String::from("Transmutation")
};
write!(f, "{}", text)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CastingTime
{
Seconds(u16),
Actions(u16),
BonusAction(Option<String>),
Reaction(Option<String>),
Minutes(u16),
Hours(u16),
Days(u16),
Weeks(u16),
Months(u16),
Years(u16),
Special
}
impl fmt::Display for CastingTime
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
fn action_type_circumstance_string(circumstance: &Option<String>, action_type_str: String) -> String
{
match circumstance
{
Some(circumstance_str) => format!("{}, {}", action_type_str, circumstance_str),
None => action_type_str
}
}
let bonus_action_str = String::from("Bonus action");
let reaction_str = String::from("Reaction");
let text = match self
{
Self::Seconds(t) => get_amount_string(*t, "second"),
Self::Actions(t) =>
{
if *t == 1 { String::from("Action") }
else { format!("{} actions", t) }
},
Self::BonusAction(circumstance) =>
{
action_type_circumstance_string(circumstance, bonus_action_str)
},
Self::Reaction(circumstance) =>
{
action_type_circumstance_string(circumstance, reaction_str)
},
Self::Minutes(t) => get_amount_string(*t, "minute"),
Self::Hours(t) => get_amount_string(*t, "hour"),
Self::Days(t) => get_amount_string(*t, "day"),
Self::Weeks(t) => get_amount_string(*t, "week"),
Self::Months(t) => get_amount_string(*t, "month"),
Self::Years(t) => get_amount_string(*t, "year"),
Self::Special => String::from("Special")
};
write!(f, "{}", text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Distance
{
Feet(u16),
Miles(u16)
}
impl Distance
{
pub fn get_aoe_string(&self) -> String
{
match self
{
Self::Feet(d) => format!("{}-foot", d),
Self::Miles(d) => format!("{}-mile", d)
}
}
}
impl fmt::Display for Distance
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
let text = match self
{
Self::Feet(d) => format!("{} feet", d),
Self::Miles(d) => format!("{} miles", d)
};
write!(f, "{}", text)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Aoe
{
Line(Distance),
Cone(Distance),
Cube(Distance),
Sphere(Distance),
Emanation(Distance),
Hemisphere(Distance),
Cylinder(Distance, Distance),
}
impl fmt::Display for Aoe
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
let text = match self
{
Self::Line(l) => format!("{} line", l.get_aoe_string()),
Self::Cone(l) => format!("{} cone", l.get_aoe_string()),
Self::Cube(l) => format!("{} cube", l.get_aoe_string()),
Self::Sphere(r) => format!("{} radius", r.get_aoe_string()),
Self::Emanation(r) => format!("{} emanation", r.get_aoe_string()),
Self::Hemisphere(r) => format!("{} radius hemisphere", r.get_aoe_string()),
Self::Cylinder(r, h) => format!("{} radius, {} height cylinder", r.get_aoe_string(), h.get_aoe_string())
};
write!(f, "{}", text)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Range
{
Yourself(Option<Aoe>),
Touch,
Dist(Distance),
Sight,
Unlimited,
Special
}
impl fmt::Display for Range
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
let text = match self
{
Self::Yourself(o) =>
{
match o
{
None => String::from("Self"),
Some(a) => format!("Self ({})", a)
}
}
Self::Touch => String::from("Touch"),
Self::Dist(d) => d.to_string(),
Self::Sight => String::from("Sight"),
Self::Unlimited => String::from("Unlimited"),
Self::Special => String::from("Special")
};
write!(f, "{}", text)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Duration
{
Instant,
Seconds(u16, bool),
Rounds(u16, bool),
Minutes(u16, bool),
Hours(u16, bool),
Days(u16, bool),
Weeks(u16, bool),
Months(u16, bool),
Years(u16, bool),
DispelledOrTriggered(bool),
UntilDispelled(bool),
Permanent,
Special(bool)
}
impl fmt::Display for Duration
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
{
let text = match self
{
Self::Instant => String::from("Instantaneous"),
Self::Seconds(t, c) =>
{
let s = get_amount_string(*t, "second");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Rounds(t, c) =>
{
let s = get_amount_string(*t, "round");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Minutes(t, c) =>
{
let s = get_amount_string(*t, "minute");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Hours(t, c) =>
{
let s = get_amount_string(*t, "hour");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Days(t, c) =>
{
let s = get_amount_string(*t, "day");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Weeks(t, c) =>
{
let s = get_amount_string(*t, "week");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Months(t, c) =>
{
let s = get_amount_string(*t, "month");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::Years(t, c) =>
{
let s = get_amount_string(*t, "year");
if *c { format!("Concentration, up to {}", s) }
else { s }
},
Self::DispelledOrTriggered(c) =>
{
let s = String::from("Until dispelled or triggered");
if *c { format!("Concentration, up {}", s) }
else { s }
}
Self::UntilDispelled(c) =>
{
let s = String::from("Until dispelled");
if *c { format!("Concentration, up {}", s) }
else { s }
}
Self::Permanent => String::from("Permanent"),
Self::Special(c) =>
{
let s = String::from("Special");
if *c {format!("Concentration, {}", s) }
else { s }
}
};
write!(f, "{}", text)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Table
{
pub title: String,
pub column_labels: Vec<String>,
pub cells: Vec<Vec<String>>
}
fn get_amount_string(num: u16, unit: &str) -> String
{
if num == 1
{
format!("1 {}", unit)
}
else
{
format!("{} {}s", num, unit)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Spell
{
pub name: String,
pub level: SpellField<Level>,
pub school: SpellField<MagicSchool>,
pub is_ritual: bool,
pub casting_time: SpellField<CastingTime>,
pub range: SpellField<Range>,
pub has_v_component: bool,
pub has_s_component: bool,
pub m_components: Option<String>,
pub duration: SpellField<Duration>,
pub description: String,
pub upcast_description: Option<String>,
pub tables: Vec<Table>
}
impl Spell
{
pub fn from_json_file(file_path: &str) -> Result<Self, Box<dyn error::Error>>
{
let file = fs::File::open(file_path)?;
let reader = BufReader::new(file);
let spell = from_reader(reader)?;
Ok(spell)
}
pub fn to_json_file(&self, file_path: &str, compress: bool) -> Result<(), Box<dyn error::Error>>
{
let file = fs::File::create(file_path)?;
if compress { to_writer(file, self)?; }
else { to_writer_pretty(file, self)?; }
Ok(())
}
pub fn get_component_string(&self) -> String
{
let mut component_string = String::new();
if self.has_v_component
{
component_string += "V";
}
if self.has_s_component
{
if component_string.len() > 0
{
component_string += ", ";
}
component_string += "S";
}
if let Some(m) = &self.m_components
{
if component_string.len() > 0
{
component_string += ", ";
}
component_string += format!("M ({})", m).as_str();
}
if component_string.is_empty() { component_string = "None".to_string(); }
component_string
}
pub fn get_level_school_text(&self) -> String
{
let text = match &self.level
{
SpellField::Controlled(Level::Cantrip) => format!("{} {}", &self.school, &self.level),
_ => format!("{} {}", &self.level, &self.school)
};
text
}
pub fn get_casting_time_text(&self) -> String
{
if self.is_ritual { format!("{} or Ritual", self.casting_time) }
else { self.casting_time.to_string() }
}
}