Skip to content

Commit

Permalink
Merge branch 'health_and_wp'
Browse files Browse the repository at this point in the history
  • Loading branch information
Ablesius committed Oct 23, 2024
2 parents c835ce6 + ba93d01 commit bf6287d
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 79 deletions.
43 changes: 37 additions & 6 deletions src/character.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod attributes;
pub mod skills;
pub mod stats;

use crate::character::stats::{Damage, Health, Willpower};
use anyhow::Result;
pub use attributes::Attribute;
use attributes::Attributes;
Expand All @@ -18,13 +20,19 @@ pub struct Character {

attributes: Attributes,
skills: Skills,

damage: Damage,
willpower_damage: Damage,
}

impl Character {
/// Create a new Character.
/// You can provide attributes and skills or leave them blank (by explicitly passing `None`);
/// with `None`, the default values will be set (0 for attributes and (0, None) for skills;
/// see `Skills`.
/// see [Skills].
///
/// **Note**: We assume that a new character does not have any damage;
/// that would have to be set later.
pub fn new(
player_name: String,
character_name: String,
Expand All @@ -38,6 +46,8 @@ impl Character {
chronicle,
attributes: attributes.unwrap_or_default(),
skills: skills.unwrap_or_default(),
damage: Damage::default(),
willpower_damage: Damage::default(),
}
}

Expand All @@ -48,7 +58,7 @@ impl Character {
/// ```json
/// "{\"name\":\"Foo\",\"brawl_skill\":[0,null]}"
///```
/// (In other words, `null` represents what is `Option::None` as serialized.
/// (In other words, `null` represents what is `Option::None` as serialized)
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Character> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Expand All @@ -73,13 +83,28 @@ impl Character {
println!("Chronicle: {}", self.chronicle);
// TODO: print all the fields
}

//TODO do we need this rather?
fn _get_max_health(&self) -> u8 {
Health::from_character(
self,
Some(self.damage.superficial),
Some(self.damage.aggravated),
)
.value
}

// TODO same as above
fn _get_max_wp(&self) -> u8 {
Willpower::from_character(self).value
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attributes_and_skills_unset() {
fn new_character_all_default_values() {
let test_char = Character::new(
String::from("Test Player"),
String::from("Test Character"),
Expand All @@ -95,7 +120,9 @@ mod tests {
character_name: String::from("Test Character"),
chronicle: String::from("Test Chronicle by Night"),
attributes: Attributes::default(),
skills: Default::default(),
skills: Skills::default(),
damage: Damage::default(),
willpower_damage: Damage::default(),
}
);
}
Expand Down Expand Up @@ -136,7 +163,9 @@ mod tests {
wits: 3,
resolve: 2,
},
skills: Default::default(),
skills: Skills::default(),
damage: Damage::default(),
willpower_damage: Damage::default(),
};

assert_eq!(test_char, expected);
Expand Down Expand Up @@ -186,7 +215,7 @@ mod tests {
player_name: String::from(""),
character_name: String::from(""),
chronicle: String::from(""),
attributes: Default::default(),
attributes: Attributes::default(),
skills: Skills {
athletics: (1, None),
brawl: (2, None),
Expand Down Expand Up @@ -216,6 +245,8 @@ mod tests {
science: (2, None),
technology: (3, None),
},
damage: Damage::default(),
willpower_damage: Damage::default(),
};

assert_eq!(test_char, expected);
Expand Down
57 changes: 40 additions & 17 deletions src/character/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ impl Attributes {
self.wits = 2;
self.resolve = 2;
}

/// Set attributes during character creation.
/// The rules require that there's one attribute at 4 dots, one at 1, three at 3, and the rest
/// at 2.
pub fn set_attributes_during_creation(
&mut self,
highest: Attribute,
lowest: Attribute,
three_threes: Vec<Attribute>,
) {
self.set_all_to_2();
self[highest] = 4;
self[lowest] = 1;

for attr in three_threes {
self[attr] = 3;
}
}
}

/// We can use this to do something like
Expand Down Expand Up @@ -138,21 +156,26 @@ impl Display for ParseAttributeError {

impl std::error::Error for ParseAttributeError {}

#[test]
fn set_all_to_two_works() {
let mut test_attributes: Attributes = Default::default();
let expected = Attributes {
strength: 2,
dexterity: 2,
stamina: 2,
charisma: 2,
manipulation: 2,
composure: 2,
intelligence: 2,
wits: 2,
resolve: 2,
};

test_attributes.set_all_to_2();
assert_eq!(test_attributes, expected);
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn set_all_to_two_works() {
let mut test_attributes = Attributes::default();
let expected = Attributes {
strength: 2,
dexterity: 2,
stamina: 2,
charisma: 2,
manipulation: 2,
composure: 2,
intelligence: 2,
wits: 2,
resolve: 2,
};

test_attributes.set_all_to_2();
assert_eq!(test_attributes, expected);
}
}
58 changes: 14 additions & 44 deletions src/character/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,6 @@ pub struct Skills {
pub technology: (u8, Option<String>),
}

// impl Default for Skills {
// fn default() -> Self {
// Self {
// athletics: (0, None),
// brawl: (0, None),
// craft: (0, None),
// drive: (0, None),
// firearms: (0, None),
// larceny: (0, None),
// melee: (0, None),
// stealth: (0, None),
// survival: (0, None),
// animal_ken: (0, None),
// etiquette: (0, None),
// insight: (0, None),
// intimidation: (0, None),
// leadership: (0, None),
// performance: (0, None),
// persuasion: (0, None),
// streetwise: (0, None),
// subterfuge: (0, None),
// academics: (0, None),
// awareness: (0, None),
// finance: (0, None),
// investigation: (0, None),
// medicine: (0, None),
// occult: (0, None),
// politics: (0, None),
// science: (0, None),
// technology: (0, None),
// }
// }
// }

/// We can use this to do something like
/// let skill: Skill = Skill::Brawl/* say you get this from user input ... */;
/// character.skills\[brawl] += 1;
Expand Down Expand Up @@ -190,16 +156,20 @@ impl Display for ParseSkillError {

impl std::error::Error for ParseSkillError {}

#[test]
fn skill_from_string() {
let skill = "athletics".parse::<Skill>().unwrap();
let expected = Skill::Athletics;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skill_from_string() {
let skill = "athletics".parse::<Skill>().unwrap();
let expected = Skill::Athletics;

assert_eq!(skill, expected)
}
assert_eq!(skill, expected)
}

#[test]
#[should_panic]
fn non_existing_skill_from_string() {
let _ = "foo".parse::<Skill>().unwrap();
#[test]
#[should_panic]
fn non_existing_skill_from_string() {
let _ = "foo".parse::<Skill>().unwrap();
}
}
119 changes: 119 additions & 0 deletions src/character/stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::character::{Attribute, Character};
use serde::{Deserialize, Serialize};

#[derive(Default, Debug, PartialEq)]
pub struct Health {
pub value: u8,
pub damage: Damage,
}

impl Health {
pub fn new(value: u8) -> Self {
Self {
value,
damage: Damage::default(),
}
}

/// Create a Health struct from a Character's values.
/// Health in VtM is calculated as 3 + Stamina,
/// so we only need to extract Stamina from the
/// Attributes field.
pub fn from_character(
character: &Character,
superficial: Option<u8>,
aggravated: Option<u8>,
) -> Self {
let damage = Damage {
superficial: superficial.unwrap_or_default(),
aggravated: aggravated.unwrap_or_default(),
};

Self {
value: &character.attributes[Attribute::Stamina] + 3,
damage,
}
}
}

/// Rather `Damage` than health since we will always be able to calculate max
/// health at runtime. Superficial and aggravated damage, however, must be
/// tracked on the sheet.
#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
pub struct Damage {
pub superficial: u8,
pub aggravated: u8,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Willpower {
pub value: u8,
pub damage: Damage,
}

impl Willpower {
/// Willpower is calculated from a character's Composure + Resolve values
pub fn from_character(character: &Character) -> Self {
Self {
value: character.attributes[Attribute::Composure]
+ character.attributes[Attribute::Resolve],
damage: Damage {
superficial: character.willpower_damage.superficial,
aggravated: character.willpower_damage.aggravated,
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn health_from_character() {
let char = Character::from_file(std::path::PathBuf::from(
"tests/sample_character_dir/sample_char.json",
))
.expect("sample_char.json should contain valid character json!");

let expected_health = Health::new(5);

assert_eq!(Health::from_character(&char, None, None), expected_health)
}

#[test]
fn health_calculated_correctly() {
use crate::character::attributes::Attributes;
let mut attributes = Attributes::default();
attributes[Attribute::Stamina] = 3;

let char = Character::new(
String::from("Juke"),
String::from("Mx Anderson"),
String::from("Cthulhu by Night"),
Some(attributes),
None,
);

let health = Health::from_character(&char, None, None);
// health should be Stamina + 3, so in this case 6
let expected = 6;

assert_eq!(health.value, expected);
}

#[test]
fn willpower_from_character() {
let char = Character::from_file(std::path::PathBuf::from(
"tests/sample_character_dir/sample_char.json",
))
.expect("sample_char.json should contain valid character json!");

let expected_wp = Willpower {
value: 5,
damage: Damage::default(),
};

assert_eq!(Willpower::from_character(&char), expected_wp);
}
}
Loading

0 comments on commit bf6287d

Please sign in to comment.