diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1327b9..4ead8368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Changelog ## Unreleased +### Added +* Customizable player and enemy classes #76 ### Removed * Backwards compatibility code for binary game data from v0.4.0 #75 +### Changed +* `rpg reset --hard` removes datafile instead of entire .rpg dir 5adfb87 + ## [0.5.0](https://github.com/facundoolano/rpg-cli/releases/tag/0.5.0) - 2021-06-26 ### Added * a `rpg reset --hard` flag to remove data files and forget information from previous plays #46 diff --git a/Cargo.lock b/Cargo.lock index 1d3a5914..4052f8ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "dunce" version = "1.0.2" @@ -119,9 +125,9 @@ checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" [[package]] name = "erased-serde" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b36e6f2295f393f44894c6031f67df4d185b984cd54d08f768ce678007efcd" +checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa" dependencies = [ "serde", ] @@ -150,9 +156,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "heck" @@ -165,18 +171,18 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", @@ -218,9 +224,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "once_cell" @@ -284,9 +296,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", "rand_chacha", @@ -306,27 +318,27 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ "rand_core", ] [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" dependencies = [ "bitflags", ] @@ -354,6 +366,7 @@ dependencies = [ "rand", "serde", "serde_json", + "serde_yaml", "typetag", ] @@ -394,6 +407,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "strsim" version = "0.10.0" @@ -455,9 +480,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" @@ -519,3 +544,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index bb42db9f..1de5093a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ typetag = "0.1" dunce = "1.0.1" once_cell = "1.7.2" serde_json = "1.0.64" +serde_yaml = "0.8" diff --git a/README.md b/README.md index 380fc045..6d198579 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Features: * Automatic turn-based combat. * Item and equipment support. * 15+ enemy classes. +* Extensible player and enemy classes via configuration. * Quests to-do list. * Chests hidden in directories. * Permadeath with item recovering. @@ -170,6 +171,12 @@ you can recover gold, items and equipment: Try `rpg --help` for more options and check the [shell integration guide](shell/README.md) for ideas to adapt the game to your preferences. +## Customize character classes + +The character class determines a character's initial stats and at what pace they increase when leveling up. By default, rpg-cli will use classes as defined by [this file](src/character/classes.yaml), but these definitions can be overridden by placing a YAML file with that same structure at `~/.rpg/classes.yaml`. + +The `category` field is used to distinguish between player and enemy classes, and in the latter case how likely a given enemy class is likely to appear (e.g. `legendary` classes will appear less frequently, and only when far away from home). + ## Troubleshooting * The release binary for macOS [is not signed](https://github.com/facundoolano/rpg-cli/issues/27). To open it for the first time, right click on the binary and select "Open" from the menu. diff --git a/src/character/class.rs b/src/character/class.rs index 28d17475..f37d2e21 100644 --- a/src/character/class.rs +++ b/src/character/class.rs @@ -1,10 +1,13 @@ use crate::location; +use once_cell::sync::OnceCell; use rand::prelude::SliceRandom; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; /// A stat represents an attribute of a character, such as strength or speed. /// This struct contains a stat starting value and the amount that should be /// applied when the level increases. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Stat(pub i32, pub i32); impl Stat { @@ -24,199 +27,110 @@ impl Stat { /// Classes are archetypes for characters. /// The struct contains a specific stat configuration such that all instances of /// the class have a similar combat behavior. -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Class { - pub name: &'static str, + pub name: String, pub hp: Stat, pub strength: Stat, pub speed: Stat, + pub category: Category, + pub inflicts: Option<(super::StatusEffect, u32)>, } +/// Determines whether the class is intended for a Player or, if it's for an enemy, +/// How rare it is (how frequently it should appear). +/// Enables easier customization of the classes via an external file. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, std::hash::Hash)] +#[serde(rename_all = "snake_case")] +pub enum Category { + Player, + Common, + Rare, + Legendary, +} + +static CLASSES: OnceCell>> = OnceCell::new(); + impl Class { - pub const HERO: Self = Self { - name: "hero", - hp: Stat(30, 7), - strength: Stat(12, 3), - speed: Stat(11, 2), - inflicts: None, - }; + /// Customize the classes definitions based on an input yaml byte array. + pub fn load(bytes: &[u8]) { + CLASSES.set(from_bytes(bytes)).unwrap(); + } + + /// The default player class, exposed for initialization and parameterization of + /// items and equipment. + pub fn warrior() -> &'static Self { + CLASSES + .get_or_init(default_classes) + .get(&Category::Player) + .unwrap() + .get(0) + .unwrap() + } - pub fn random_enemy(distance: location::Distance) -> &'static Self { + pub fn random_enemy(distance: location::Distance) -> Self { weighted_choice(distance) } + + pub fn enemy_names(group: Category) -> HashSet { + CLASSES + .get_or_init(default_classes) + .get(&group) + .unwrap() + .iter() + .map(|class| class.name.clone()) + .collect() + } +} + +fn default_classes() -> HashMap> { + from_bytes(include_bytes!("classes.yaml")) } -pub const COMMON: &[Class] = &[RAT, WOLF, SNAKE, SLIME, SPIDER]; -pub const RARE: &[Class] = &[ZOMBIE, ORC, SKELETON, DEMON, VAMPIRE, DRAGON, GOLEM]; -pub const LEGENDARY: &[Class] = &[CHIMERA, BASILISK, MINOTAUR, BALROG, PHOENIX]; +fn from_bytes(bytes: &[u8]) -> HashMap> { + // it would arguably be better for these module not to deal with deserialization + // and yaml, but at this stage it's easier allow it to pick up defaults from + // the local file when it hasn't been customized (especially for tests) + let mut classes: Vec = serde_yaml::from_slice(bytes).unwrap(); + + let mut class_groups = HashMap::new(); + for class in classes.drain(..) { + let entry = class_groups + .entry(class.category.clone()) + .or_insert_with(Vec::new); + entry.push(class); + } + class_groups +} /// Choose an enemy randomly, with higher chance to difficult enemies the further from home. -fn weighted_choice(distance: location::Distance) -> &'static Class { +fn weighted_choice(distance: location::Distance) -> Class { // the weights for each group of enemies are different depending on the distance // the further from home, the bigger the chance to find difficult enemies - let (w_near, w_mid, w_far) = match distance { + let (w_common, w_rare, w_legendary) = match distance { location::Distance::Near(_) => (9, 2, 0), location::Distance::Mid(_) => (7, 10, 1), location::Distance::Far(_) => (1, 6, 3), }; - // assign weights to each group - let near = COMMON.iter().map(|c| (c, w_near)); - let mid = RARE.iter().map(|c| (c, w_mid)); - let far = LEGENDARY.iter().map(|c| (c, w_far)); - - // make a weighted random choice let mut rng = rand::thread_rng(); - near.chain(mid) - .chain(far) - .collect::>() + + // assign weights to each group and select one + let weights = vec![ + (Category::Common, w_common), + (Category::Rare, w_rare), + (Category::Legendary, w_legendary), + ]; + let category = &weights .as_slice() .choose_weighted(&mut rng, |(_c, weight)| *weight) .unwrap() - .0 -} + .0; -// NOTE: we shouldn't end up in a place were the hero raises its value and as -// a consequence the enemies raise it too, making them unbeatable. -// Consider: 1. raising the enemy level solely (or primarily) based on distance; -// 2. decreasing rates to prevent overgrowth at higher levels -// as a starting measure, using increase rates way below those of the player - -const RAT: Class = Class { - name: "rat", - hp: Stat(10, 3), - strength: Stat(5, 2), - speed: Stat(16, 2), - inflicts: None, -}; - -const WOLF: Class = Class { - name: "wolf", - hp: Stat(15, 3), - strength: Stat(8, 2), - speed: Stat(12, 2), - inflicts: None, -}; - -const SNAKE: Class = Class { - name: "snake", - hp: Stat(13, 3), - strength: Stat(7, 2), - speed: Stat(6, 2), - inflicts: Some((super::StatusEffect::Poisoned, 5)), -}; - -const SLIME: Class = Class { - name: "slime", - hp: Stat(80, 3), - strength: Stat(3, 2), - speed: Stat(4, 2), - inflicts: Some((super::StatusEffect::Poisoned, 10)), -}; - -const SPIDER: Class = Class { - name: "spider", - hp: Stat(10, 3), - strength: Stat(9, 2), - speed: Stat(12, 2), - inflicts: Some((super::StatusEffect::Poisoned, 20)), -}; - -const ZOMBIE: Class = Class { - name: "zombie", - hp: Stat(50, 3), - strength: Stat(8, 2), - speed: Stat(6, 2), - inflicts: None, -}; - -const ORC: Class = Class { - name: "orc", - hp: Stat(35, 3), - strength: Stat(13, 2), - speed: Stat(12, 2), - inflicts: None, -}; - -const SKELETON: Class = Class { - name: "skeleton", - hp: Stat(30, 3), - strength: Stat(10, 2), - speed: Stat(10, 2), - inflicts: None, -}; - -const DEMON: Class = Class { - name: "demon", - hp: Stat(50, 3), - strength: Stat(10, 2), - speed: Stat(18, 2), - inflicts: Some((super::StatusEffect::Burning, 10)), -}; - -const VAMPIRE: Class = Class { - name: "vampire", - hp: Stat(50, 3), - strength: Stat(13, 2), - speed: Stat(10, 2), - inflicts: None, -}; - -const DRAGON: Class = Class { - name: "dragon", - hp: Stat(100, 3), - strength: Stat(25, 2), - speed: Stat(8, 2), - inflicts: Some((super::StatusEffect::Burning, 2)), -}; - -const GOLEM: Class = Class { - name: "golem", - hp: Stat(50, 3), - strength: Stat(45, 2), - speed: Stat(2, 1), - inflicts: None, -}; - -const CHIMERA: Class = Class { - name: "chimera", - hp: Stat(200, 2), - strength: Stat(90, 2), - speed: Stat(16, 2), - inflicts: Some((super::StatusEffect::Poisoned, 3)), -}; - -const BASILISK: Class = Class { - name: "basilisk", - hp: Stat(150, 3), - strength: Stat(100, 2), - speed: Stat(18, 2), - inflicts: Some((super::StatusEffect::Poisoned, 2)), -}; - -const MINOTAUR: Class = Class { - name: "minotaur", - hp: Stat(100, 3), - strength: Stat(60, 2), - speed: Stat(40, 2), - inflicts: None, -}; - -const BALROG: Class = Class { - name: "balrog", - hp: Stat(200, 3), - strength: Stat(200, 2), - speed: Stat(14, 2), - inflicts: Some((super::StatusEffect::Burning, 3)), -}; - -const PHOENIX: Class = Class { - name: "phoenix", - hp: Stat(350, 3), - strength: Stat(180, 2), - speed: Stat(28, 2), - inflicts: Some((super::StatusEffect::Burning, 2)), -}; + // get a random class within the group + let classes = CLASSES.get().unwrap().get(category).unwrap(); + classes.choose(&mut rng).unwrap().clone() +} diff --git a/src/character/classes.yaml b/src/character/classes.yaml new file mode 100644 index 00000000..4cdee009 --- /dev/null +++ b/src/character/classes.yaml @@ -0,0 +1,99 @@ +- name: warrior + hp: [30, 7] + strength: [12, 3] + speed: [11, 2] + category: player +- name: rat + hp: [10, 3] + strength: [5, 2] + speed: [16, 2] + category: common +- name: wolf + hp: [15, 3] + strength: [8, 2] + speed: [12, 2] + category: common +- name: snake + hp: [13, 3] + strength: [7, 2] + speed: [6, 2] + inflicts: [poisoned, 5] + category: common +- name: slime + hp: [80, 3] + strength: [3, 2] + speed: [4, 2] + inflicts: [poisoned, 10] + category: common +- name: spider + hp: [10, 3] + strength: [9, 2] + speed: [12, 2] + inflicts: [poisoned, 20] + category: common +- name: zombie + hp: [50, 3] + strength: [8, 2] + speed: [6, 2] + category: rare +- name: orc + hp: [35, 3] + strength: [13, 2] + speed: [12, 2] + category: rare +- name: skeleton + hp: [30, 3] + strength: [10, 2] + speed: [10, 2] + category: rare +- name: demon + hp: [50, 3] + strength: [10, 2] + speed: [18, 2] + inflicts: [burning, 10] + category: rare +- name: vampire + hp: [50, 3] + strength: [13, 2] + speed: [10, 2] + category: rare +- name: dragon + hp: [100, 3] + strength: [25, 2] + speed: [8, 2] + inflicts: [burning, 2] + category: rare +- name: golem + hp: [50, 3] + strength: [45, 2] + speed: [2, 1] + category: rare +- name: chimera + hp: [200, 2] + strength: [90, 2] + speed: [16, 2] + inflicts: [poisoned, 3] + category: legendary +- name: basilisk + hp: [150, 3] + strength: [100, 2] + speed: [18, 2] + inflicts: [poisoned, 2] + category: legendary +- name: minotaur + hp: [100, 3] + strength: [60, 2] + speed: [40, 2] + category: legendary +- name: balrog + hp: [200, 3] + strength: [200, 2] + speed: [14, 2] + inflicts: [burning, 3] + category: legendary +- name: phoenix + hp: [350, 3] + strength: [180, 2] + speed: [28, 2] + inflicts: [burning, 3] + category: legendary diff --git a/src/character/mod.rs b/src/character/mod.rs index 6057dfd5..90c0c63c 100644 --- a/src/character/mod.rs +++ b/src/character/mod.rs @@ -11,8 +11,7 @@ pub mod class; #[derive(Serialize, Deserialize, Debug)] #[serde(default)] pub struct Character { - #[serde(skip, default = "default_class")] - class: &'static Class, + pub class: Class, pub sword: Option, pub shield: Option, @@ -28,6 +27,7 @@ pub struct Character { } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[serde(rename_all = "snake_case")] pub enum StatusEffect { Burning, Poisoned, @@ -41,14 +41,9 @@ impl Default for Character { } } -// Always attach the static hero class to deserialized characters -fn default_class() -> &'static Class { - &Class::HERO -} - impl Character { pub fn player() -> Self { - Self::new(&Class::HERO, 1) + Self::new(Class::warrior().clone(), 1) } pub fn enemy(level: i32, distance: location::Distance) -> Self { @@ -60,21 +55,24 @@ impl Character { } pub fn is_player(&self) -> bool { - // kind of ugly but does the job - self.class.name == "hero" + self.class.category == class::Category::Player } - fn new(class: &'static Class, level: i32) -> Self { + fn new(class: Class, level: i32) -> Self { + let max_hp = class.hp.base(); + let current_hp = class.hp.base(); + let strength = class.strength.base(); + let speed = class.speed.base(); let mut character = Self { class, sword: None, shield: None, level: 1, xp: 0, - max_hp: class.hp.base(), - current_hp: class.hp.base(), - strength: class.strength.base(), - speed: class.speed.base(), + max_hp, + current_hp, + strength, + speed, status_effect: None, }; @@ -217,16 +215,18 @@ mod tests { use super::*; use class::Stat; - const TEST_CLASS: Class = Class { - name: "test", - hp: Stat(25, 7), - strength: Stat(10, 3), - speed: Stat(10, 2), - inflicts: None, - }; - fn new_char() -> Character { - Character::new(&TEST_CLASS, 1) + Character::new( + Class { + name: "test".to_string(), + category: class::Category::Player, + hp: Stat(25, 7), + strength: Stat(10, 3), + speed: Stat(10, 2), + inflicts: None, + }, + 1, + ) } #[test] @@ -236,10 +236,10 @@ mod tests { assert_eq!(1, hero.level); assert_eq!(0, hero.xp); - assert_eq!(TEST_CLASS.hp.base(), hero.current_hp); - assert_eq!(TEST_CLASS.hp.base(), hero.max_hp); - assert_eq!(TEST_CLASS.strength.base(), hero.strength); - assert_eq!(TEST_CLASS.speed.base(), hero.speed); + assert_eq!(hero.class.hp.base(), hero.current_hp); + assert_eq!(hero.class.hp.base(), hero.max_hp); + assert_eq!(hero.class.strength.base(), hero.strength); + assert_eq!(hero.class.speed.base(), hero.speed); assert!(hero.status_effect.is_none()); } @@ -248,9 +248,9 @@ mod tests { let mut hero = new_char(); // assert what we're assuming are the params in the rest of the test - assert_eq!(7, TEST_CLASS.hp.increase()); - assert_eq!(3, TEST_CLASS.strength.increase()); - assert_eq!(2, TEST_CLASS.speed.increase()); + assert_eq!(7, hero.class.hp.increase()); + assert_eq!(3, hero.class.strength.increase()); + assert_eq!(2, hero.class.speed.increase()); hero.max_hp = 20; hero.current_hp = 20; diff --git a/src/datafile.rs b/src/datafile.rs index 3d73519e..86f05f37 100644 --- a/src/datafile.rs +++ b/src/datafile.rs @@ -1,42 +1,53 @@ +use crate::character::class; use crate::game; use std::{fs, io, path}; pub struct NotFound; pub fn load() -> Result { - let data: Vec = read()?; + let data: Vec = read(data_file())?; let game = serde_json::from_slice(&data).unwrap(); Ok(game) } pub fn save(game: &game::Game) -> Result<(), io::Error> { let data = serde_json::to_vec(game).unwrap(); - write(data) + write(data_file(), data) } pub fn remove() { let rpg_dir = rpg_dir(); if rpg_dir.exists() { - fs::remove_dir_all(&rpg_dir).unwrap(); + fs::remove_file(data_file()).unwrap(); } } -fn read() -> Result, NotFound> { - fs::read(file()).map_err(|_| NotFound) +pub fn load_classes() { + if let Ok(bytes) = read(classes_file()) { + class::Class::load(&bytes) + } +} + +fn read(file: path::PathBuf) -> Result, NotFound> { + fs::read(file).map_err(|_| NotFound) } -fn write(data: Vec) -> Result<(), io::Error> { +fn write(file: path::PathBuf, data: Vec) -> Result<(), io::Error> { let rpg_dir = rpg_dir(); if !rpg_dir.exists() { fs::create_dir(&rpg_dir).unwrap(); } - fs::write(file(), &data) + fs::write(file, &data) } fn rpg_dir() -> path::PathBuf { dirs::home_dir().unwrap().join(".rpg") } -fn file() -> path::PathBuf { +fn data_file() -> path::PathBuf { rpg_dir().join("data") } + +fn classes_file() -> path::PathBuf { + rpg_dir().join("classes.yaml") +} diff --git a/src/item/equipment.rs b/src/item/equipment.rs index 67ea6c22..d15b136d 100644 --- a/src/item/equipment.rs +++ b/src/item/equipment.rs @@ -12,7 +12,7 @@ pub trait Equipment: fmt::Display { /// the item is equipped. fn strength(&self) -> i32 { // get the base strength of the hero at this level - let player_strength = character::Class::HERO.strength.at(self.level()); + let player_strength = character::Class::warrior().strength.at(self.level()); // calculate the added strength as a function of the player strength (player_strength as f64 * 0.5).round() as i32 diff --git a/src/item/mod.rs b/src/item/mod.rs index 6040365e..e9c48c38 100644 --- a/src/item/mod.rs +++ b/src/item/mod.rs @@ -33,7 +33,7 @@ impl fmt::Display for Potion { #[typetag::serde] impl Item for Potion { fn apply(&self, game: &mut game::Game) { - let to_restore = character::Class::HERO.hp.at(self.level) / 2; + let to_restore = character::Class::warrior().hp.at(self.level) / 2; let recovered = game.player.heal(to_restore); Event::emit( diff --git a/src/main.rs b/src/main.rs index 17ef50c6..ca27617e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,6 +110,8 @@ fn main() { datafile::remove(); } + datafile::load_classes(); + let mut game = datafile::load().unwrap_or_else(|_| Game::new()); match opts.cmd.unwrap_or(Command::Stat) { diff --git a/src/quest/beat_enemy.rs b/src/quest/beat_enemy.rs index dc173110..3a0dd0f9 100644 --- a/src/quest/beat_enemy.rs +++ b/src/quest/beat_enemy.rs @@ -1,15 +1,17 @@ use std::collections::HashSet; use super::Quest; +use crate::character::class; use crate::character::class::Class; use crate::event::Event; use serde::{Deserialize, Serialize}; -pub fn of_class(classes: &[Class], description: &str) -> Box { - let to_beat = classes.iter().map(|c| c.name.to_string()).collect(); +pub fn of_class(category: class::Category, description: &str) -> Box { + let to_beat = Class::enemy_names(category); + let total = to_beat.len(); Box::new(BeatEnemyClass { to_beat, - total: classes.len(), + total, description: description.to_string(), }) } diff --git a/src/quest/mod.rs b/src/quest/mod.rs index 770400bf..afef0206 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -1,4 +1,4 @@ -use crate::character; +use crate::character::class; use crate::event; use crate::game; use crate::log; @@ -47,7 +47,7 @@ impl QuestList { self.todo.push(( 2, 1000, - beat_enemy::of_class(&character::class::COMMON, "beat all common creatures"), + beat_enemy::of_class(class::Category::Common, "beat all common creatures"), )); self.todo.push((5, 200, Box::new(tutorial::VisitTomb))); @@ -56,14 +56,14 @@ impl QuestList { self.todo.push(( 5, 5000, - beat_enemy::of_class(&character::class::RARE, "beat all rare creatures"), + beat_enemy::of_class(class::Category::Rare, "beat all rare creatures"), )); self.todo.push((5, 1000, beat_enemy::at_distance(10))); self.todo.push(( 10, 10000, - beat_enemy::of_class(&character::class::LEGENDARY, "beat all common creatures"), + beat_enemy::of_class(class::Category::Legendary, "beat all common creatures"), )); } @@ -124,11 +124,12 @@ impl fmt::Display for dyn Quest { #[cfg(test)] mod tests { use super::*; + use crate::character::Character; #[test] fn test_quest_completed() { let mut game = game::Game::new(); - let fake_enemy = character::Character::player(); + let fake_enemy = Character::player(); let initial_quests = game.quests.todo.len(); assert!(initial_quests > 0);