From eacf0c1a0138fc374127dc6b6ccbd4f94d1c06be Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 11 Aug 2026 14:36:51 +0200 Subject: [PATCH] feat: initial commit - item catalog, player and stats --- .gitignore | 1 + Cargo.lock | 152 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +++ src/items.rs | 108 +++++++++++++++++++++++++++++ src/items/armor.rs | 46 +++++++++++++ src/items/armors.toml | 13 ++++ src/items/weapon.rs | 62 +++++++++++++++++ src/items/weapons.toml | 15 ++++ src/main.rs | 20 ++++++ src/player.rs | 144 ++++++++++++++++++++++++++++++++++++++ src/stats.rs | 108 +++++++++++++++++++++++++++++ 11 files changed, 677 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/items.rs create mode 100644 src/items/armor.rs create mode 100644 src/items/armors.toml create mode 100644 src/items/weapon.rs create mode 100644 src/items/weapons.toml create mode 100644 src/main.rs create mode 100644 src/player.rs create mode 100644 src/stats.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f104322 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,152 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustkeep" +version = "0.1.0" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..026b010 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rustkeep" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0.229", features = ["derive"] } +toml = "1.1.4" diff --git a/src/items.rs b/src/items.rs new file mode 100644 index 0000000..0910b79 --- /dev/null +++ b/src/items.rs @@ -0,0 +1,108 @@ +use serde::Deserialize; +use serde::de::DeserializeOwned; + +pub mod armor; +pub mod weapon; + +pub use armor::ItemArmor; +pub use weapon::ItemWeapon; + +// === ENUMS === +#[derive(Clone, Debug, Deserialize)] +pub enum ItemType { + Weapon, + OffHand, + Helmet, + Chestplate, + Gloves, + Boots, + Consumable, + Currency, +} + +// === STRUCTS === +#[derive(Deserialize)] +pub struct Item { + id: String, + name: String, + value: u32, + item_type: ItemType, +} + +/// Rohform einer Item-Datei. Jede TOML nutzt `[[items]]` als Wurzel, +/// dadurch bedient dieser eine Wrapper alle Dateien. +#[derive(Deserialize)] +struct ItemFile { + items: Vec, +} + +/// Laufzeit-Registry: einmal beim Start laden, danach als `&ItemCatalog` durchreichen. +// TODO: OffHand, Consumable und Currency haben noch keine Structs/TOMLs — hier ergaenzen. +pub struct ItemCatalog { + pub weapons: Vec, + pub armors: Vec, +} + +// === TRAITS === +pub trait ItemTrait { + fn get_id(&self) -> String; + fn get_name(&self) -> String; + fn get_value(&self) -> u32; + fn get_type(&self) -> ItemType; +} + +// === IMPLEMENTS === +impl ItemTrait for Item { + fn get_id(&self) -> String { + self.id.clone() + } + + fn get_name(&self) -> String { + self.name.clone() + } + + fn get_value(&self) -> u32 { + self.value + } + + fn get_type(&self) -> ItemType { + self.item_type.clone() + } +} + +// === FUNCTIONS === +pub fn load_items() -> ItemCatalog { + ItemCatalog { + weapons: parse(include_str!("items/weapons.toml"), "weapons.toml"), + armors: parse(include_str!("items/armors.toml"), "armors.toml"), + } +} + +fn parse(raw: &str, file: &str) -> Vec { + let parsed: ItemFile = + toml::from_str(raw).unwrap_or_else(|e| panic!("{} ist kaputt:\n{}", file, e)); + parsed.items +} + +/// Rahmen um beliebig viele Zeilen. Wird von jedem `print_values` genutzt. +pub(crate) fn print_box(lines: &[String]) { + let longest: usize = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0); + + println!("+-{}-+", "-".repeat(longest)); + for line in lines { + println!("| {}{} |", line, " ".repeat(longest - line.chars().count())); + } + println!("+-{}-+", "-".repeat(longest)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn item_files_parse() { + let catalog: ItemCatalog = load_items(); + assert!(!catalog.weapons.is_empty()); + assert!(!catalog.armors.is_empty()); + } +} diff --git a/src/items/armor.rs b/src/items/armor.rs new file mode 100644 index 0000000..dd6bf29 --- /dev/null +++ b/src/items/armor.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +use super::{Item, ItemTrait, ItemType, print_box}; + +#[derive(Deserialize)] +pub struct ItemArmor { + armor: u32, + #[serde(flatten)] + item: Item, +} + +impl ItemTrait for ItemArmor { + fn get_id(&self) -> String { + self.item.get_id() + } + + fn get_name(&self) -> String { + self.item.get_name() + } + + fn get_value(&self) -> u32 { + self.item.get_value() + } + + fn get_type(&self) -> ItemType { + self.item.get_type() + } +} + +impl ItemArmor { + pub fn get_armor(&self) -> u32 { + self.armor + } + + pub fn print_values(&self) { + let lines: Vec = vec![ + format!("Stats for {} ", self.get_name()), + format!("ID: {}", self.get_id()), + format!("Slot: {:?}", self.get_type()), + format!("Armor: {}", self.get_armor()), + format!("Value: {}", self.get_value()), + ]; + + print_box(&lines); + } +} diff --git a/src/items/armors.toml b/src/items/armors.toml new file mode 100644 index 0000000..fe8bccd --- /dev/null +++ b/src/items/armors.toml @@ -0,0 +1,13 @@ +[[items]] +id = "iron_helmet" +name = "Iron Helmet" +armor = 4 +value = 6 +item_type = "Helmet" + +[[items]] +id = "leather_boots" +name = "Leather Boots" +armor = 2 +value = 3 +item_type = "Boots" diff --git a/src/items/weapon.rs b/src/items/weapon.rs new file mode 100644 index 0000000..9ca559a --- /dev/null +++ b/src/items/weapon.rs @@ -0,0 +1,62 @@ +use serde::Deserialize; + +use super::{Item, ItemTrait, ItemType, print_box}; + +#[derive(Deserialize)] +pub struct ItemWeapon { + base_damage: u32, + critical_factor: f32, + #[serde(flatten)] + item: Item, +} + +impl ItemTrait for ItemWeapon { + fn get_id(&self) -> String { + self.item.get_id() + } + + fn get_name(&self) -> String { + self.item.get_name() + } + + fn get_value(&self) -> u32 { + self.item.get_value() + } + + fn get_type(&self) -> ItemType { + self.item.get_type() + } +} + +impl ItemWeapon { + pub fn get_base_damage(&self) -> u32 { + self.base_damage + } + + pub fn get_critical_factor(&self) -> f32 { + self.critical_factor + } + + pub fn get_critical_factor_readable(&self) -> String { + let factor: i32 = ((self.get_critical_factor() - 1.0) * 100.0).round() as i32; + format!("{}%", factor) + } + + pub fn print_values(&self) { + let crit_dmg: u32 = + (self.get_base_damage() as f32 * self.get_critical_factor()).ceil() as u32; + + let lines: Vec = vec![ + format!("Stats for {} ", self.get_name()), + format!("ID: {}", self.get_id()), + format!("Base Damage: {} ({})", self.get_base_damage(), crit_dmg), + format!( + "Critical Factor: {}", + self.get_critical_factor_readable() + ), + format!("Value: {}", self.get_value()), + ]; + + print_box(&lines); + } +} diff --git a/src/items/weapons.toml b/src/items/weapons.toml new file mode 100644 index 0000000..a936998 --- /dev/null +++ b/src/items/weapons.toml @@ -0,0 +1,15 @@ +[[items]] +id = "iron_sword" +name = "Iron Sword" +base_damage = 10 +critical_factor = 1.1 +value = 5 +item_type = "Weapon" + +[[items]] +id = "iron_dagger" +name = "Iron Dagger" +base_damage = 8 +critical_factor = 1.5 +value = 3 +item_type = "Weapon" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c34aa72 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +mod items; +mod player; +mod stats; + +use items::*; +use player::*; + +fn main() { + let catalog: ItemCatalog = load_items(); + + for weapon in &catalog.weapons { + weapon.print_values(); + } + for armor in &catalog.armors { + armor.print_values(); + } + + let player: Player = Player::new("Reya".to_string()); + player.show_stats(); +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..909a91e --- /dev/null +++ b/src/player.rs @@ -0,0 +1,144 @@ +use std::fmt::format; + +use crate::stats::{Stats, StatsTrait}; + +pub struct Player { + name: String, + stats: Stats, + next_level_exp: usize, + current_exp: usize, +} + +pub trait PlayerTrait { + fn get_name(&self) -> String; + fn set_name(&mut self, name: String); + fn get_current_exp(&self) -> usize; + fn get_next_level_exp(&self) -> usize; + fn show_stats(&self); +} + +impl Player { + pub fn new(name: String) -> Self { + Self { + name, + stats: Stats::new(), + next_level_exp: 10, + current_exp: 0, + } + } +} + +impl PlayerTrait for Player { + fn get_name(&self) -> String { + self.name.clone() + } + + fn set_name(&mut self, name: String) { + self.name = name; + } + + fn show_stats(&self) { + let name: String = format!("Name: {}", self.get_name()); + let level: String = format!( + "Level: {} ({}/{})", + self.get_level(), + self.get_current_exp(), + self.get_next_level_exp() + ); + let strength: String = format!("Strength: {}", self.get_strengh()); + let dexterity: String = format!("Dexterity: {}", self.get_dexterity()); + let constitution: String = format!("Constitution: {}", self.get_constitution()); + let charisma: String = format!("Charisma: {}", self.get_charisma()); + let luck: String = format!("Luck: {}", self.get_luck()); + + let stats_data: [&String; 7] = [ + &name, + &level, + &strength, + &dexterity, + &constitution, + &charisma, + &luck, + ]; + let mut longest: usize = 0; + for data in stats_data { + let length: usize = data.chars().count(); + longest = length.max(longest); + } + + let spacer: String = format!("+-{}-+", "-".repeat(longest)); + + println!("{}", spacer); + for data in stats_data { + let length: usize = data.chars().count(); + println!("| {}{} |", data, " ".repeat(longest - length)); + } + println!("{}", spacer); + } + + fn get_current_exp(&self) -> usize { + self.current_exp + } + + fn get_next_level_exp(&self) -> usize { + self.next_level_exp + } +} + +impl StatsTrait for Player { + fn get_max_health(&self) -> f32 { + self.stats.get_max_health() + } + + fn get_current_health(&self) -> f32 { + self.stats.get_current_health() + } + + fn set_current_health(&mut self, value: f32) { + self.stats.set_current_health(value); + } + + fn is_dead(&self) -> bool { + self.stats.is_dead() + } + + fn get_max_stamina(&self) -> f32 { + self.stats.get_max_stamina() + } + + fn get_current_stamina(&self) -> f32 { + self.stats.get_current_stamina() + } + + fn set_current_stamina(&mut self, value: f32) { + self.stats.set_current_stamina(value); + } + + fn get_strengh(&self) -> usize { + self.stats.get_strengh() + } + + fn get_dexterity(&self) -> usize { + self.stats.get_dexterity() + } + + fn get_constitution(&self) -> usize { + self.stats.get_constitution() + } + + fn get_charisma(&self) -> usize { + self.stats.get_charisma() + } + + fn get_luck(&self) -> usize { + self.stats.get_luck() + } + + fn get_level(&self) -> usize { + self.stats.get_level() + } + + fn level_up(&mut self) { + self.stats.level_up(); + } +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..090b792 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,108 @@ +pub struct Stats { + max_health: f32, + current_health: f32, + max_stamina: f32, + current_stamina: f32, + strength: usize, // Determines base_damage modifier + dexterity: usize, // Determines Critical Factor Modifier + constitution: usize, // Determines Max Health Bonuses + charisma: usize, // Determines Shop Prices + luck: usize, // Determines Loot Drop & Critical Chance + level: usize, +} + +pub trait StatsTrait { + fn get_max_health(&self) -> f32; + fn get_current_health(&self) -> f32; + fn set_current_health(&mut self, value: f32); + fn is_dead(&self) -> bool; + fn get_max_stamina(&self) -> f32; + fn get_current_stamina(&self) -> f32; + fn set_current_stamina(&mut self, value: f32); + fn get_strengh(&self) -> usize; + fn get_dexterity(&self) -> usize; + fn get_constitution(&self) -> usize; + fn get_charisma(&self) -> usize; + fn get_luck(&self) -> usize; + fn get_level(&self) -> usize; + fn level_up(&mut self); +} + +impl Stats { + pub fn new() -> Self { + let health: f32 = 100.0; + let stamina: f32 = 100.0; + Self { + max_health: health, + current_health: health, + max_stamina: stamina, + current_stamina: stamina, + strength: 1, + dexterity: 1, + constitution: 1, + charisma: 1, + luck: 1, + level: 1, + } + } +} + +impl StatsTrait for Stats { + fn get_max_health(&self) -> f32 { + self.max_health + } + + fn get_current_health(&self) -> f32 { + self.current_health + } + + fn set_current_health(&mut self, value: f32) { + let new_val: f32 = self.current_health + value; + self.current_health = new_val.clamp(0.0, self.max_health); + } + + fn is_dead(&self) -> bool { + self.current_health <= 0.0 + } + + fn get_max_stamina(&self) -> f32 { + self.max_stamina + } + + fn get_current_stamina(&self) -> f32 { + self.current_stamina + } + + fn set_current_stamina(&mut self, value: f32) { + let new_val: f32 = self.current_stamina + value; + self.current_stamina = new_val.clamp(0.0, self.max_stamina); + } + + fn get_strengh(&self) -> usize { + self.strength + } + + fn get_dexterity(&self) -> usize { + self.dexterity + } + + fn get_constitution(&self) -> usize { + self.constitution + } + + fn get_charisma(&self) -> usize { + self.charisma + } + + fn get_luck(&self) -> usize { + self.luck + } + + fn get_level(&self) -> usize { + self.level + } + + fn level_up(&mut self) { + self.level = (self.level + 1).clamp(1, 50); + } +}