dnd_spellbook_maker 0.7.0

Library for making pdf documents of 5th edition D&D spells that are formatted like D&D source books.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Data structures for defining, creating, and using spells
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

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};

/// Holds spell fields with either a controlled value or a custom value represented by a string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(private_bounds)]
pub enum SpellField<T: fmt::Display>
{
	/// Controlled values are meant to be fields with limited values so invalid data cannot be displayed.
	///
	/// Controlled values must implement Display so the spell struct can display them without converting them manually.
	Controlled(T),
	/// Custom values allow for anything to be displayed in the spellbook.
	///
	/// Custom values appear in spell files as text surrounded by quotes in a field that otherwise has controlled values.
	Custom(String)
}

// Converts SpellFields into strings
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)
		}
	}
}

/// The level of a spell.
// 0 is a cantrip, max level is 9
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Level
{
	Cantrip,
	Level1,
	Level2,
	Level3,
	Level4,
	Level5,
	Level6,
	Level7,
	Level8,
	Level9
}

// Converts levels into strings
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)
	}
}

// Allows Levels to be created from integers (u8) for easier usage
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).")
		}
	}
}

// Converts spell levels into integers (u8)
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)
	}
}

/// The school of magic a spell belongs to
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum MagicSchool
{
	Abjuration,
	Conjuration,
	Divination,
	Enchantment,
	Evocation,
	Illusion,
	Necromancy,
	Transmutation
}

// Allows strings of magic schools to be converted to the MagicSchool type
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.")
		}
	}
}

// Converts magic schools into strings
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)
	}
}

/// The amount of time it takes to cast a spell.
///
/// u16 values are the number of units of time it takes to cast the spell,
/// variants are the unit of time.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CastingTime
{
	Seconds(u16),
	Actions(u16),
	/// String is the circumstance in which the bonus action can be triggered.
	/// Use `None` for no specific circumstance.
	/// Circumstance Examples: "which you take when you or a creature within 60 feet of you falls" or
	/// "which you take when you see a creature within 60 feet of you casting a spell".
	///
	/// Note: if this value is `Some`, its string will come after the string "Bonus action, " on the spell page.
	BonusAction(Option<String>),
	/// String is the circumstance in which the reaction can be triggered.
	/// Use `None` for no specific circumstance.
	/// Circumstance Examples: "which you take when you or a creature within 60 feet of you falls" or
	/// "which you take when you see a creature within 60 feet of you casting a spell".
	///
	/// Note: if this value is `Some`, its string will come after the string "Reaction, " on the spell page.
	Reaction(Option<String>),
	Minutes(u16),
	Hours(u16),
	Days(u16),
	Weeks(u16),
	Months(u16),
	Years(u16),
	Special
}

// Converts casting times into strings
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)
	}
}

/// Holds a distance value. The enum variant determine its unit of measurement.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Distance
{
	Feet(u16),
	Miles(u16)
}

impl Distance
{
	/// Returns a string of the distance in the format of "d-u" where
	/// d is the distance value and u is the unit of measurement.
	///
	/// Used in displaying distances for Aoe.
	pub fn get_aoe_string(&self) -> String
	{
		match self
		{
			Self::Feet(d) => format!("{}-foot", d),
			Self::Miles(d) => format!("{}-mile", d)
		}
	}
}

// Converts Distances into strings
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)
	}
}

/// Area of Effect.
/// The volumnetric shape in which a spell's effect(s) take place.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Aoe
{
	/// Distance defines length of line (width should be in spell description).
	Line(Distance),
	/// Distance defines length / height and diameter of cone.
	Cone(Distance),
	/// Distance defines the length of the edges of the cube.
	Cube(Distance),
	/// Distance defines radius of sphere (same thing functionally as emanation in game rules).
	Sphere(Distance),
	/// Distance defines radius of emanation (same thing functionally as emanation in game rules).
	Emanation(Distance),
	/// Distance defines radius of hemisphere.
	Hemisphere(Distance),
	/// Distances define radius and height of cylinder (respectively).
	Cylinder(Distance, Distance),
}

// Converts Aoes into strings
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)
	}
}

/// The farthest distance away a spell can target things.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Range
{
	/// The Aoe option in this variant is for spells that have areas of effect that come from the spellcaster.
	/// Ex: "Burning Hands" has a range of "Self (15-foot cone)".
	Yourself(Option<Aoe>),
	Touch,
	/// This variant is for plain distance ranges like "60 feet" or "5 miles".
	Dist(Distance),
	Sight,
	Unlimited,
	Special
}

// Converts spell ranges into strings
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)
	}
}

/// The length of time a spell's effect(s) lasts.
///
/// u16 values are the number of units the spell can last.
/// Bool values are whether or not the spell requires concentration.
#[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)
}

// Converts spell durations into strings
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)
	}
}

/// Holds a table that goes in a spellbook description.
/// It does not need to be a perfect square, jagged tables are allowed.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Table
{
	/// The title text that goes above the table. Leave as empty string for no title.
	pub title: String,
	/// The labels above each column on the first row of the table.
	/// Leave entire vec empty for no column labels and individual strings empty to skip over a column.
	pub column_labels: Vec<String>,
	/// Vec of the text that goes in each individual cell in the table. Outer vec is the row of the cell (up and
	/// down placement), inner vec is the column of the cell (left to right placement). Lower row indexes mean higher
	/// up vertically on the table, lower column indexes mean more to the left.
	pub cells: Vec<Vec<String>>
}

// Gets a string of an amount of something like "1 minute", "5 minutes", "1 hour", or "3 hours"
// Note: the unit should be singular, not plural because an 's' will be added to the end of it if num is anything but 1
fn get_amount_string(num: u16, unit: &str) -> String
{
	if num == 1
	{
		format!("1 {}", unit)
	}
	else
	{
		format!("{} {}s", num, unit)
	}
}

/// Data containing all of the information about a spell needed to display it in a spellbook.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Spell
{
	pub name: String,
	/// Can be custom value or Level.
	pub level: SpellField<Level>,
	/// Can be custom value or MagicSchool.
	pub school: SpellField<MagicSchool>,
	/// Whether or not the spell can be casted as a ritual.
	pub is_ritual: bool,
	/// Can be custom value or CastingTime.
	pub casting_time: SpellField<CastingTime>,
	/// Can be custom value or Range.
	pub range: SpellField<Range>,
	/// Whether or not the spell requires a verbal component to be cast.
	pub has_v_component: bool,
	/// Whether or not the spell requires a somantic component to be cast.
	pub has_s_component: bool,
	/// Text that lists the material components a spell might need to be cast.
	/// A value of `None` represents the spell not needing any material components.
	pub m_components: Option<String>,
	/// Can be custom value or Duration.
	pub duration: SpellField<Duration>,
	/// Text that describes the effects of the spell.
	/// 
	/// Can be formatted with font changing tags, bullet points, and tables.
	///
	/// See spell file documentation for more information (<https://github.com/ChandlerJayCalkins/dnd_spellbook_maker>).
	pub description: String,
	/// Optional text that describes the benefits a spell gains from being upcast (being cast at a level higher than
	/// its base level if it's a non-cantrip or being cast by a character higher than a certain level if its a
	/// cantrip).
	pub upcast_description: Option<String>,
	/// Any tables that the spell might have in its description
	pub tables: Vec<Table>
}

impl Spell
{
	/// Constructs a spell object from a json file.
	///
	/// # Parameters
	///
	/// - `file_path` The path to the json file to create the spell from.
	///
	/// # Output
	///
	/// - `Ok` A spell object.
	/// - `Err` Any errors that occured.
	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)
	}

	/// Saves a spell to a json file.
	///
	/// # Parameters
	///
	/// - `file_path` The file path to save the spell to.
	/// - `compress` True to put all the data onto one line, false to make the file more human readable.
	///
	/// # Output
	///
	/// - `Ok` Nothing if there were no errors.
	/// - `Err` Any errors that occurred.
	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(())
	}

	/// Gets a string of the required components for a spell.
	///
	/// Ex: "V, S, M (a bit of sulfur and some wood bark)", "V, S", "V, M (a piece of hair)".
	pub fn get_component_string(&self) -> String
	{
		let mut component_string = String::new();
		// If there is a v component
		if self.has_v_component
		{
			// Add a v to the string
			component_string += "V";
		}
		// If there is an s component
		if self.has_s_component
		{
			// If there is at least 1 component already
			if component_string.len() > 0
			{
				// Add a comma to the string
				component_string += ", ";
			}
			// Add an s to the string
			component_string += "S";
		}
		// If there is an m component
		if let Some(m) = &self.m_components
		{
			// If there is at least 1 component already
			if component_string.len() > 0
			{
				// Add a comma to the string
				component_string += ", ";
			}
			// Add the m component(s) to the string
			component_string += format!("M ({})", m).as_str();
		}

		// If there are no components, set the string to "None"
		if component_string.is_empty() { component_string = "None".to_string(); }
		// Return the string
		component_string
	}

	/// Gets the school and level info from a spell and turns it into text that says something like "nth-Level School-Type".
	///
	/// Ex: "1st-Level abjuration", "8th-Level transmutation", "evocation cantrip".
	pub fn get_level_school_text(&self) -> String
	{
		// Gets a string of the level and the school from the spell
		let text = match &self.level
		{
			// If the spell is a cantrip, make the school come first and then the level
			SpellField::Controlled(Level::Cantrip) => format!("{} {}", &self.school, &self.level),
			// If the spell is any other level or a custom value, make the level come before the school
			_ => format!("{} {}", &self.level, &self.school)
		};
		// Return the string
		text
	}

	/// Gets the casting time and ritual info from a spell and turns it into text that says something like
	/// "1 action or Ritual", "1 bonus action", or "2 hours"
	pub fn get_casting_time_text(&self) -> String
	{
		// If the spell is a ritual, return the casting time with "or Ritual" at the end of it
		if self.is_ritual { format!("{} or Ritual", self.casting_time) }
		// If the spell is not a ritual, just return the casting time
		else { self.casting_time.to_string() }
	}
}