15 Commits

13 changed files with 657 additions and 115 deletions

38
Cargo.lock generated
View File

@@ -155,9 +155,9 @@ checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.11.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
dependencies = [ dependencies = [
"either", "either",
] ]
@@ -315,9 +315,9 @@ dependencies = [
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.24.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425" checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"cassowary", "cassowary",
@@ -326,6 +326,7 @@ dependencies = [
"itertools", "itertools",
"lru", "lru",
"paste", "paste",
"stability",
"strum", "strum",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
@@ -388,6 +389,16 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "stability"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.25.0" version = "0.25.0"
@@ -407,7 +418,18 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn", "syn 2.0.38",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
] ]
[[package]] [[package]]
@@ -472,7 +494,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.38",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -494,7 +516,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.38",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -630,5 +652,5 @@ checksum = "56feebc4664bdb37f39c0b1b8fc8fa2f5648e3ec5f9cf4344978b4b7ca561fff"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.38",
] ]

View File

@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
ratatui = "0.24.0" ratatui = "0.25.0"
crossterm = "0.27.0" crossterm = "0.27.0"
rand = "0.8.5" rand = "0.8.5"
petgraph = "0.6.4" petgraph = "0.6.4"

7
build.rs Normal file
View File

@@ -0,0 +1,7 @@
use std::process::Command;
fn main() {
let output = Command::new("git").args(&["rev-parse", "--short", "HEAD"]).output().unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
}

View File

@@ -9,7 +9,7 @@ pub trait Artifact {
/// get the position of the artifact in the level /// get the position of the artifact in the level
fn get_immutable_position(&self) -> &Position; fn get_immutable_position(&self) -> &Position;
/// call to apply the effects of the artifact to the player /// call to apply the effects of the artifact to the player
fn collect(&mut self, player: &mut Player); fn collect(&mut self, player: &mut Player, messages: &mut Vec<String>);
/// returns if the artifact was collected and can be removed from the level /// returns if the artifact was collected and can be removed from the level
fn was_collected(&self) -> bool; fn was_collected(&self) -> bool;
} }
@@ -37,8 +37,9 @@ impl Artifact for Chest {
} }
fn get_immutable_position(&self) -> &Position { &self.position } fn get_immutable_position(&self) -> &Position { &self.position }
fn collect(&mut self, player: &mut Player) { fn collect(&mut self, player: &mut Player, messages: &mut Vec<String>) {
player.retrieve_gold(self.gold); player.retrieve_gold(self.gold);
messages.insert(0, format!("opened chest and collected {} gold.", self.gold).to_string());
self.gold = 0; self.gold = 0;
} }
@@ -67,9 +68,17 @@ impl Artifact for Potion {
("P", Color::Green) ("P", Color::Green)
} }
fn get_immutable_position(&self) -> &Position { &self.position } fn get_immutable_position(&self) -> &Position { &self.position }
fn collect(&mut self, player: &mut Player) { fn collect(&mut self, player: &mut Player, messages: &mut Vec<String>) {
player.change_life(self.health.try_into().unwrap()); // only consume potion of the player can gain at least one health point
self.health = 0; if !player.is_healthy() {
let old = player.get_life();
player.increase_life(self.health.try_into().unwrap());
let new = player.get_life();
messages.insert(0, format!("picked up potion and gained {} hp.", new - old).to_string());
self.health = 0;
} else {
messages.insert(0, "not using the potion because you're healthy.".to_string());
}
} }
fn was_collected(&self) -> bool { fn was_collected(&self) -> bool {

150
src/dungeon_slayer.rs Normal file
View File

@@ -0,0 +1,150 @@
use rand::Rng;
pub fn do_challenge(stat: u8) -> (bool, u8) {
if stat <= 20 {
let mut rng = rand::thread_rng();
let dice_roll = rng.gen_range(1..21);
return match dice_roll {
20 => { (false, 0) }
1 => { (true, dice_roll) }
_ => {
if dice_roll <= stat {
(true, stat - dice_roll)
} else {
(false, 0)
}
}
};
}
(false, 0)
}
#[allow(dead_code)]
pub struct PlayerStats {
// dungeon slayer attributes
pub(crate) body: u8,
pub(crate) agility: u8,
pub(crate) mind: u8,
// dungeon slayer properties
// for body
pub(crate) strength: u8,
pub(crate) toughness: u8,
// for agility
pub(crate) movement: u8,
pub(crate) dexterity: u8,
// for mind
pub(crate) wisdom: u8,
// TODO maybe the wrong translation of Verstand
pub(crate) aura: u8,
}
impl PlayerStats {
/// create random values for the 3 attributes of the dungeon slayer character
/// Each attribute is between 4 and 8, not over 20 in sum.
fn create_random_attributes() -> (u8, u8, u8) {
let mut values: [u8; 3] = [8, 8, 8];
let mut rng = rand::thread_rng();
while values[0] + values[1] + values[2] > 20 {
let i = rng.gen_range(0..3);
values[i] = values[i].saturating_sub(1);
}
(values[0], values[1], values[2])
}
/// create random values for the 6 properties of the dungeon slayer character
/// Each property is between 0 and 4, not over 8 in sum.
fn create_random_properties() -> (u8, u8, u8, u8, u8, u8) {
let mut values: [u8; 6] = [4, 4, 4, 4, 4, 4];
let mut rng = rand::thread_rng();
while values[0] + values[1] + values[2] + values[3] + values[4] + values[5] > 8 {
let i = rng.gen_range(0..6);
values[i] = values[i].saturating_sub(1);
}
(values[0], values[1], values[2], values[3], values[4], values[5])
}
/// create a set of player stats containing attributes and properties according to the
/// dungeon slayer rules.
pub fn create_random() -> PlayerStats {
let attributes = PlayerStats::create_random_attributes();
let properties = PlayerStats::create_random_properties();
PlayerStats {
body: attributes.0,
agility: attributes.1,
mind: attributes.2,
strength: properties.0,
toughness: properties.1,
movement: properties.2,
dexterity: properties.3,
wisdom: properties.4,
aura: properties.5,
}
}
/// calculate the max life based on this player stats.
pub fn get_max_life(&self) -> u8 {
self.body + self.toughness + 10
}
pub fn get_defense(&self) -> u8 {
self.body + self.toughness // TODO + self.armor
}
pub fn get_hit(&self) -> u8 {
self.body + self.strength // TODO + self.weapon
}
}
#[allow(dead_code)]
pub struct MonsterStats {
// dungeon slayer attributes
pub(crate) body: u8,
pub(crate) agility: u8,
pub(crate) mind: u8,
// dungeon slayer properties
// for body
pub(crate) strength: u8,
pub(crate) toughness: u8,
// for agility
pub(crate) movement: u8,
pub(crate) dexterity: u8,
// for mind
pub(crate) wisdom: u8,
// TODO maybe the wrong translation of Verstand
pub(crate) aura: u8,
pub(crate) max_life: u8,
pub(crate) defense: u8,
pub(crate) initiative: u8,
pub(crate) walk: u8,
pub(crate) hit: u8,
pub(crate) shoot: u8,
pub(crate) cast: u8,
pub(crate) targeted_cast: u8,
}
#[test]
fn test_create_random_attributes() {
for _ in 0..1000 {
let attributes = PlayerStats::create_random_attributes();
assert!(attributes.0 <= 8);
assert!(attributes.1 <= 8);
assert!(attributes.2 <= 8);
assert_eq!(attributes.0 + attributes.1 + attributes.2, 20);
}
}
#[test]
fn test_create_random_properties() {
for _ in 0..1000 {
let properties = PlayerStats::create_random_properties();
assert!(properties.0 <= 4);
assert!(properties.1 <= 4);
assert!(properties.2 <= 4);
assert!(properties.3 <= 4);
assert!(properties.4 <= 4);
assert!(properties.5 <= 4);
assert_eq!(properties.0 + properties.1 + properties.2 + properties.3 + properties.4 + properties.5, 8);
}
}

View File

@@ -1,3 +1,6 @@
use crate::dungeon_slayer::do_challenge;
#[cfg(test)]
use crate::dungeon_slayer::PlayerStats;
use crate::level::{Level, StructureElement}; use crate::level::{Level, StructureElement};
use crate::level_generator::LevelGenerator; use crate::level_generator::LevelGenerator;
use crate::player::Player; use crate::player::Player;
@@ -23,6 +26,8 @@ pub struct Game {
player: Player, player: Player,
/// the levels of the game /// the levels of the game
levels: Vec<Level>, levels: Vec<Level>,
pub messages: Vec<String>,
use_fog_of_war: bool,
} }
impl Game { impl Game {
@@ -34,6 +39,8 @@ impl Game {
let mut g = Game { let mut g = Game {
player: p, player: p,
levels: v, levels: v,
messages: Vec::with_capacity(10),
use_fog_of_war: true,
}; };
let start = { let start = {
g.get_level(0).start g.get_level(0).start
@@ -69,6 +76,8 @@ impl Game {
pub fn get_player(&self) -> &Player { pub fn get_player(&self) -> &Player {
&self.player &self.player
} }
pub fn toggle_fog_of_war(&mut self) { self.use_fog_of_war = !self.use_fog_of_war; }
pub fn uses_fog_of_war(&self) -> bool { self.use_fog_of_war }
pub fn get_mutable_player(&mut self) -> &mut Player { pub fn get_mutable_player(&mut self) -> &mut Player {
&mut self.player &mut self.player
} }
@@ -100,7 +109,7 @@ impl Game {
let new_y: i16 = player_pos.get_y() as i16 + dy; let new_y: i16 = player_pos.get_y() as i16 + dy;
let player_level = player_pos.get_level(); let player_level = player_pos.get_level();
let level = &mut self.levels[player_level]; let level = &mut self.levels[player_level];
level.get_element(new_x, new_y).0 level.get_element(new_x, new_y, self.use_fog_of_war).0
} }
/// move the game's player into the given direction. If on the target field there is a stair, /// move the game's player into the given direction. If on the target field there is a stair,
@@ -125,12 +134,14 @@ impl Game {
(dx, dy) = (0, 0); (dx, dy) = (0, 0);
let (next_level, x, y) = self.next_start(); let (next_level, x, y) = self.next_start();
player_level = next_level; player_level = next_level;
self.messages.insert(0, format!("You climb down to level {}.", next_level));
self.get_mutable_player().get_position().set(next_level, x, y); self.get_mutable_player().get_position().set(next_level, x, y);
} }
StructureElement::StairUp => { StructureElement::StairUp => {
(dx, dy) = (0, 0); (dx, dy) = (0, 0);
let (next_level, x, y) = self.prev_end(); let (next_level, x, y) = self.prev_end();
player_level = next_level; player_level = next_level;
self.messages.insert(0, format!("You climb up to level {}.", next_level));
self.get_mutable_player().get_position().set(next_level, x, y); self.get_mutable_player().get_position().set(next_level, x, y);
} }
_ => {} _ => {}
@@ -148,16 +159,27 @@ impl Game {
let player_pos = &self.player.get_immutable_position(); let player_pos = &self.player.get_immutable_position();
let player_level = player_pos.get_level(); let player_level = player_pos.get_level();
let level = &mut self.levels[player_level]; let level = &mut self.levels[player_level];
let (_, m, _) = level.get_element(player_pos.get_x() as i16, player_pos.get_y() as i16); let (_, m, _) = level.get_element(player_pos.get_x() as i16, player_pos.get_y() as i16, true);
match m { match m {
None => {} None => {}
Some(m) => { Some(m) => {
// TODO fight the monster // Player attacks monster
self.player.change_life(-1); let attack = do_challenge(self.player.player_stats.get_hit());
m.decrease_life(1); if attack.0 {
let mut damage = attack.1;
let defense = do_challenge(m.get_defense());
if defense.0 {
damage = damage.saturating_sub(defense.1);
}
self.messages.insert(0, format!("Attack on {} successful with {} damage.", m.get_name(), damage).to_string());
m.decrease_life(damage as usize);
} else {
self.messages.insert(0, format!("Attack on {} failed.", m.get_name()).to_string());
}
// monster died, player gains experience // monster died, player gains experience
if m.is_dead() { if m.is_dead() {
self.player.gain_experience(m.get_experience_gain()); self.player.gain_experience(m.get_experience_gain());
self.messages.insert(0, format!("You killed the {}.", m.get_name()).to_string());
} }
return m.is_dead(); return m.is_dead();
} }
@@ -168,11 +190,11 @@ impl Game {
let player_pos = &self.player.get_immutable_position(); let player_pos = &self.player.get_immutable_position();
let player_level = player_pos.get_level(); let player_level = player_pos.get_level();
let level = &mut self.levels[player_level]; let level = &mut self.levels[player_level];
let (_, _, a) = level.get_element(player_pos.get_x() as i16, player_pos.get_y() as i16); let (_, _, a) = level.get_element(player_pos.get_x() as i16, player_pos.get_y() as i16, true);
match a { match a {
None => {} None => {}
Some(a) => { Some(a) => {
a.collect(&mut self.player); a.collect(&mut self.player, &mut self.messages);
} }
} }
} }
@@ -181,20 +203,42 @@ impl Game {
let player_pos = &self.player.get_immutable_position(); let player_pos = &self.player.get_immutable_position();
let player_level = player_pos.get_level(); let player_level = player_pos.get_level();
let level = &mut self.levels[player_level]; let level = &mut self.levels[player_level];
level.update(ticks, &mut self.player); level.update(ticks, &mut self.player, &mut self.messages);
} }
} }
#[test] #[test]
fn game_has_correct_number_of_levels() { fn game_has_correct_number_of_levels() {
let p = Player::new("foo", 42); let player_stats = PlayerStats {
body: 8,
agility: 8,
mind: 4,
strength: 3,
toughness: 3,
movement: 1,
dexterity: 1,
wisdom: 0,
aura: 0,
};
let p = Player::new("foo", player_stats);
let g = Game::new(p); let g = Game::new(p);
assert_eq!(g.levels.len(), LEVELS); assert_eq!(g.levels.len(), LEVELS);
} }
#[test] #[test]
fn game_has_player() { fn game_has_player() {
let p = Player::new("foo", 42); let player_stats = PlayerStats {
body: 8,
agility: 8,
mind: 4,
strength: 3,
toughness: 3,
movement: 1,
dexterity: 1,
wisdom: 0,
aura: 0,
};
let p = Player::new("foo", player_stats);
let g = Game::new(p); let g = Game::new(p);
assert_eq!(g.get_player().get_name(), "foo"); assert_eq!(g.get_player().get_name(), "foo");
assert_eq!(g.get_player().get_immutable_position().get_level(), 0); assert_eq!(g.get_player().get_immutable_position().get_level(), 0);
@@ -202,16 +246,38 @@ fn game_has_player() {
#[test] #[test]
fn game_has_mutable_player() { fn game_has_mutable_player() {
let p = Player::new("foo", 42); let player_stats = PlayerStats {
body: 8,
agility: 8,
mind: 4,
strength: 3,
toughness: 3,
movement: 1,
dexterity: 1,
wisdom: 0,
aura: 0,
};
let p = Player::new("foo", player_stats);
let mut g = Game::new(p); let mut g = Game::new(p);
assert_eq!(g.get_player().get_name(), "foo"); assert_eq!(g.get_player().get_name(), "foo");
g.get_mutable_player().change_life(-1); g.get_mutable_player().decrease_life(1);
assert_eq!(g.get_player().get_life(), 41); assert_eq!(g.get_player().get_life(), 20);
} }
#[test] #[test]
fn game_get_level() { fn game_get_level() {
let p = Player::new("foo", 42); let player_stats = PlayerStats {
body: 8,
agility: 8,
mind: 4,
strength: 3,
toughness: 3,
movement: 1,
dexterity: 1,
wisdom: 0,
aura: 0,
};
let p = Player::new("foo", player_stats);
let mut g = Game::new(p); let mut g = Game::new(p);
g.get_level(0); g.get_level(0);
assert_ne!(g.get_level(0).start, (0, 0)); assert_ne!(g.get_level(0).start, (0, 0));

View File

@@ -1,11 +1,9 @@
use std::cmp::{max, min}; use std::cmp::{max, min};
use rand::Rng;
use rand::rngs::ThreadRng;
#[cfg(test)] #[cfg(test)]
use crate::artifacts::{Chest, Potion}; use crate::artifacts::{Chest, Potion};
use crate::artifacts::Artifact; use crate::artifacts::Artifact;
use crate::dungeon_slayer::do_challenge;
#[cfg(test)] #[cfg(test)]
use crate::monster::{Orc, Rat}; use crate::monster::{Orc, Rat};
use crate::monster::Monster; use crate::monster::Monster;
@@ -37,11 +35,10 @@ pub struct Level {
pub(crate) start: (usize, usize), pub(crate) start: (usize, usize),
/// the position of the end in the level (either stair down or end point) /// the position of the end in the level (either stair down or end point)
pub(crate) end: (usize, usize), pub(crate) end: (usize, usize),
pub(crate) rng: ThreadRng,
} }
impl Level { impl Level {
pub fn get_element(&mut self, x: i16, y: i16) -> (Option<StructureElement>, Option<&mut Box<(dyn Monster + 'static)>>, Option<&mut Box<(dyn Artifact + 'static)>>) { pub fn get_element(&mut self, x: i16, y: i16, use_fog_of_war: bool) -> (Option<StructureElement>, Option<&mut Box<(dyn Monster + 'static)>>, Option<&mut Box<(dyn Artifact + 'static)>>) {
if x < 0 || y < 0 { if x < 0 || y < 0 {
return (None, None, None); return (None, None, None);
} }
@@ -50,7 +47,7 @@ impl Level {
if x >= LEVEL_WIDTH || y >= LEVEL_HEIGHT { if x >= LEVEL_WIDTH || y >= LEVEL_HEIGHT {
return (None, None, None); return (None, None, None);
} }
if !self.discovered[x][y] { if use_fog_of_war && !self.discovered[x][y] {
return (Some(StructureElement::Unknown), None, None); return (Some(StructureElement::Unknown), None, None);
} }
let search_pos = &Position::new(self.level, x, y); let search_pos = &Position::new(self.level, x, y);
@@ -112,7 +109,7 @@ impl Level {
} }
} }
} }
pub fn update(&mut self, ticks: u128, player: &mut Player) { pub fn update(&mut self, ticks: u128, player: &mut Player, messages: &mut Vec<String>) {
for (index, a) in &mut self.artifacts.iter().enumerate() { for (index, a) in &mut self.artifacts.iter().enumerate() {
if a.was_collected() { if a.was_collected() {
self.artifacts.remove(index); self.artifacts.remove(index);
@@ -128,19 +125,27 @@ impl Level {
} }
loop { loop {
// calculate the direction the monster will try to walk // calculate the direction the monster will try to walk
let (dx, dy) = match self.rng.gen_range(0..5) { let (dx, dy) = self.monsters[index].get_next_move(&self, player.get_immutable_position());
1 => { (1, 0) }
2 => { (-1, 0) }
3 => { (0, 1) }
4 => { (0, -1) }
_ => { (0, 0) }
};
if self.can_monster_move(self.monsters[index].as_ref(), dx, dy) { if self.can_monster_move(self.monsters[index].as_ref(), dx, dy) {
let (new_x, new_y) = self.monsters[index].get_position().change(dx, dy); let (new_x, new_y) = self.monsters[index].get_position().change(dx, dy);
if player.get_immutable_position().get_x() == new_x && player.get_immutable_position().get_y() == new_y { if player.get_immutable_position().get_x() == new_x && player.get_immutable_position().get_y() == new_y {
self.monsters[index].decrease_life(1); // TODO monster attacks
player.change_life(-1); let attack = do_challenge(self.monsters[index].get_hit());
// if the attack did not kill the opponent, back down if attack.0 {
let mut damage = attack.1;
let defense = do_challenge(player.player_stats.get_defense());
if defense.0 {
damage = damage.saturating_sub(defense.1);
}
player.decrease_life(damage);
if player.is_dead() {
messages.insert(0, format!("Attack from {} killed you.", self.monsters[index].get_name()).to_string());
} else {
messages.insert(0, format!("Attack from {} successful with {} damage.", self.monsters[index].get_name(), damage).to_string());
}
} else {
messages.insert(0, format!("Attack from {} failed.", self.monsters[index].get_name()).to_string());
}
if !player.is_dead() { if !player.is_dead() {
self.monsters[index].get_position().change(-dx, -dy); self.monsters[index].get_position().change(-dx, -dy);
} }
@@ -162,6 +167,7 @@ impl Level {
let new_x: usize = (agent_pos.get_x() as i16 + dx) as usize; let new_x: usize = (agent_pos.get_x() as i16 + dx) as usize;
let new_y: usize = (agent_pos.get_y() as i16 + dy) as usize; let new_y: usize = (agent_pos.get_y() as i16 + dy) as usize;
// verify we cannot leave the playing field
if new_x >= LEVEL_WIDTH || new_y >= LEVEL_HEIGHT { if new_x >= LEVEL_WIDTH || new_y >= LEVEL_HEIGHT {
return false; return false;
} }
@@ -176,6 +182,12 @@ impl Level {
let agent_pos = agent.get_immutable_position(); let agent_pos = agent.get_immutable_position();
let new_x: usize = (agent_pos.get_x() as i16 + dx) as usize; let new_x: usize = (agent_pos.get_x() as i16 + dx) as usize;
let new_y: usize = (agent_pos.get_y() as i16 + dy) as usize; let new_y: usize = (agent_pos.get_y() as i16 + dy) as usize;
// verify we cannot leave the playing field
if new_x >= LEVEL_WIDTH || new_y >= LEVEL_HEIGHT {
return false;
}
self.structure[new_x][new_y] != StructureElement::Wall self.structure[new_x][new_y] != StructureElement::Wall
} }
#[cfg(test)] #[cfg(test)]
@@ -194,7 +206,6 @@ impl Level {
artifacts: Vec::with_capacity(10), artifacts: Vec::with_capacity(10),
start: (0, 0), start: (0, 0),
end: (0, 0), end: (0, 0),
rng: rand::thread_rng(),
} }
} }
#[cfg(test)] #[cfg(test)]
@@ -228,23 +239,23 @@ impl Level {
#[test] #[test]
fn test_get_element() { fn test_get_element() {
let mut l = Level::new(0); let mut l = Level::new(0);
assert_eq!(l.get_element(-1, -1).0, None); assert_eq!(l.get_element(-1, -1, true).0, None);
assert_eq!(l.get_element(0, 0).0.unwrap(), StructureElement::Unknown); assert_eq!(l.get_element(0, 0, true).0.unwrap(), StructureElement::Unknown);
assert_eq!(l.get_element(LEVEL_WIDTH as i16 - 1, LEVEL_HEIGHT as i16 - 1).0.unwrap(), StructureElement::Unknown); assert_eq!(l.get_element(LEVEL_WIDTH as i16 - 1, LEVEL_HEIGHT as i16 - 1, true).0.unwrap(), StructureElement::Unknown);
assert_eq!(l.get_element(LEVEL_WIDTH as i16, LEVEL_HEIGHT as i16).0, None); assert_eq!(l.get_element(LEVEL_WIDTH as i16, LEVEL_HEIGHT as i16, true).0, None);
} }
#[test] #[test]
fn test_discover_get_element() { fn test_discover_get_element() {
let mut l = Level::new(0); let mut l = Level::new(0);
assert_eq!(l.get_element(10, 10).0.unwrap(), StructureElement::Unknown); assert_eq!(l.get_element(10, 10, true).0.unwrap(), StructureElement::Unknown);
let p = Position::new(0, 10, 10); let p = Position::new(0, 10, 10);
l.discover(&p); l.discover(&p);
assert_eq!(l.get_element(10, 10).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(10, 10, true).0.unwrap(), StructureElement::Floor);
assert_eq!(l.get_element(9, 10).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(9, 10, true).0.unwrap(), StructureElement::Floor);
assert_eq!(l.get_element(10, 9).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(10, 9, true).0.unwrap(), StructureElement::Floor);
assert_eq!(l.get_element(11, 10).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(11, 10, true).0.unwrap(), StructureElement::Floor);
assert_eq!(l.get_element(10, 11).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(10, 11, true).0.unwrap(), StructureElement::Floor);
} }
#[test] #[test]
@@ -285,20 +296,20 @@ fn test_discover_get_monster() {
let mut l = Level::new(0); let mut l = Level::new(0);
let p = Position::new(0, 10, 10); let p = Position::new(0, 10, 10);
l.discover(&p); l.discover(&p);
assert_eq!(l.get_element(10, 10).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(10, 10, true).0.unwrap(), StructureElement::Floor);
assert!(l.get_element(10, 10).1.is_none()); assert!(l.get_element(10, 10, true).1.is_none());
let m = Rat::new_with_position(Position::new(0, 10, 10)); let m = Rat::new_with_position(Position::new(0, 10, 10));
assert_eq!(l.add_monster(m), Ok(())); assert_eq!(l.add_monster(m), Ok(()));
let elem = l.get_element(10, 10); let elem = l.get_element(10, 10, true);
assert_eq!(elem.0.unwrap(), StructureElement::Floor); assert_eq!(elem.0.unwrap(), StructureElement::Floor);
assert!(elem.1.is_some()); assert!(elem.1.is_some());
let m = elem.1.unwrap(); let m = elem.1.unwrap();
assert_eq!(m.get_life(), 2); assert_eq!(m.get_life(), 3);
m.decrease_life(2); m.decrease_life(3);
assert_eq!(l.get_element(10, 10).1.unwrap().get_life(), 0); assert_eq!(l.get_element(10, 10, true).1.unwrap().get_life(), 0);
} }
#[test] #[test]
@@ -306,13 +317,13 @@ fn test_discover_get_artifact() {
let mut l = Level::new(0); let mut l = Level::new(0);
let p = Position::new(0, 10, 10); let p = Position::new(0, 10, 10);
l.discover(&p); l.discover(&p);
assert_eq!(l.get_element(10, 10).0.unwrap(), StructureElement::Floor); assert_eq!(l.get_element(10, 10, true).0.unwrap(), StructureElement::Floor);
assert!(l.get_element(10, 10).2.is_none()); assert!(l.get_element(10, 10, true).2.is_none());
let a = Chest::new(Position::new(0, 10, 10)); let a = Chest::new(Position::new(0, 10, 10));
assert_eq!(l.add_artifact(a), Ok(())); assert_eq!(l.add_artifact(a), Ok(()));
let elem = l.get_element(10, 10); let elem = l.get_element(10, 10, true);
assert_eq!(elem.0.unwrap(), StructureElement::Floor); assert_eq!(elem.0.unwrap(), StructureElement::Floor);
assert!(elem.2.is_some()); assert!(elem.2.is_some());
} }
@@ -326,12 +337,12 @@ fn test_discover_get_monster_can_move() {
let m = Rat::new_with_position(Position::new(0, 10, 10)); let m = Rat::new_with_position(Position::new(0, 10, 10));
l.add_monster(m).expect("Panic because of"); l.add_monster(m).expect("Panic because of");
let m = l.get_element(10, 10).1.unwrap(); let m = l.get_element(10, 10, true).1.unwrap();
m.get_position().change(1, 1); m.get_position().change(1, 1);
let m = l.get_element(10, 10).1; let m = l.get_element(10, 10, true).1;
assert!(m.is_none()); assert!(m.is_none());
let m = l.get_element(11, 11).1; let m = l.get_element(11, 11, true).1;
assert!(m.is_some()); assert!(m.is_some());
assert_eq!(m.unwrap().get_life(), 2); assert_eq!(m.unwrap().get_life(), 3);
} }

View File

@@ -11,7 +11,7 @@ use rand::rngs::ThreadRng;
use crate::artifacts::{Artifact, Chest, Potion}; use crate::artifacts::{Artifact, Chest, Potion};
use crate::level::{Level, StructureElement}; use crate::level::{Level, StructureElement};
use crate::monster::{Monster, Orc, Rat}; use crate::monster::{LowerDaemon, Monster, Rat};
use crate::position::Position; use crate::position::Position;
const ROOMS_VERTICAL: usize = 7; const ROOMS_VERTICAL: usize = 7;
@@ -276,7 +276,7 @@ impl LevelGenerator {
// TODO randomize enemies here // TODO randomize enemies here
match rng.gen_range(1..=100) { match rng.gen_range(1..=100) {
1..=50 => { 1..=50 => {
enemies.push(Box::new(Orc::new_with_position(Position::new(self.level, t_x, t_y)))); enemies.push(Box::new(LowerDaemon::new_with_position(Position::new(self.level, t_x, t_y))));
} }
_ => { _ => {
enemies.push(Box::new(Rat::new_with_position(Position::new(self.level, t_x, t_y)))); enemies.push(Box::new(Rat::new_with_position(Position::new(self.level, t_x, t_y))));
@@ -344,7 +344,6 @@ impl LevelGenerator {
artifacts, artifacts,
start: (start_x, start_y), start: (start_x, start_y),
end: (end_x, end_y), end: (end_x, end_y),
rng: rand::thread_rng(),
} }
} }
} }

View File

@@ -9,9 +9,16 @@ use crate::position::HasPosition;
const FG_BROWN: Color = Color::Rgb(186, 74, 0); const FG_BROWN: Color = Color::Rgb(186, 74, 0);
pub struct LevelWidget {} pub struct LevelWidget {
use_fog_of_war: bool,
}
impl LevelWidget { impl LevelWidget {
pub fn new(use_fog_of_war: bool) -> Self {
LevelWidget {
use_fog_of_war
}
}
fn set_cell(&self, buf: &mut Buffer, x: u16, y: u16, symbol: &str, fg: Color, bg: Color) { fn set_cell(&self, buf: &mut Buffer, x: u16, y: u16, symbol: &str, fg: Color, bg: Color) {
buf. buf.
get_mut(x, y). get_mut(x, y).
@@ -39,7 +46,7 @@ impl StatefulWidget for LevelWidget {
let level = state.get_mutable_level(player_level); let level = state.get_mutable_level(player_level);
let level_x = (x - al) as i16; let level_x = (x - al) as i16;
let level_y = (y - at) as i16; let level_y = (y - at) as i16;
match level.get_element(level_x, level_y) { match level.get_element(level_x, level_y, self.use_fog_of_war) {
(Some(structure_element), None, None) => { (Some(structure_element), None, None) => {
match structure_element { match structure_element {
StructureElement::Start => { StructureElement::Start => {

View File

@@ -8,7 +8,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
use ratatui::{ use ratatui::{
prelude::{CrosstermBackend, Stylize, Terminal}, prelude::{CrosstermBackend, Terminal},
widgets::Paragraph, widgets::Paragraph,
}; };
use ratatui::prelude::*; use ratatui::prelude::*;
@@ -16,9 +16,9 @@ use ratatui::widgets::{Block, Borders, BorderType, Wrap};
use ratatui::widgets::block::{Position, Title}; use ratatui::widgets::block::{Position, Title};
use whoami::realname; use whoami::realname;
use crate::dungeon_slayer::PlayerStats;
use crate::game::{Game, GameState}; use crate::game::{Game, GameState};
use crate::level_widget::LevelWidget; use crate::level_widget::LevelWidget;
// use crate::level_widget::LevelWidget;
use crate::player::Player; use crate::player::Player;
use crate::position::HasPosition; use crate::position::HasPosition;
@@ -30,13 +30,15 @@ mod level_widget;
mod level_generator; mod level_generator;
mod artifacts; mod artifacts;
mod monster; mod monster;
mod dungeon_slayer;
/// length of a game frame in ms /// length of a game frame in ms
pub const FRAME_LENGTH: u64 = 100; pub const FRAME_LENGTH: u64 = 100;
// //
fn main() -> Result<()> { fn main() -> Result<()> {
let mut game = Game::new(Player::new(realname().as_str(), 10)); let player_stats = PlayerStats::create_random();
let mut game = Game::new(Player::new(realname().as_str(), player_stats));
stdout().execute(EnterAlternateScreen)?; stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?; enable_raw_mode()?;
@@ -81,29 +83,67 @@ fn main() -> Result<()> {
width: level::LEVEL_WIDTH as u16, width: level::LEVEL_WIDTH as u16,
height: level::LEVEL_HEIGHT as u16, height: level::LEVEL_HEIGHT as u16,
}; };
frame.render_stateful_widget(LevelWidget {}, map_area, &mut game); frame.render_stateful_widget(LevelWidget::new(game.uses_fog_of_war()), map_area, &mut game);
let stats_area = Rect { let stats_area = Rect {
x: area.x + 50, x: area.x + 50,
y: area.y, y: area.y,
width: 30, width: 30,
height: 25, height: 15,
}; };
let block = Block::default()
.title(
Title::from(
format!(" {} ", game.get_player().get_name()))
.alignment(Alignment::Center)
.position(Position::Top)
)
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Blue));
frame.render_widget( frame.render_widget(
Paragraph::new(format!("{}\nHealth: {}/{}\nExp: {}\nGold: {}\nLevel: {}", Paragraph::new(format!("Health: {}/{}\nExp: {}\nGold: {}\nLevel: {}",
game.get_player().get_name(),
game.get_player().get_life(), game.get_player().get_life(),
game.get_player().get_max_life(), game.get_player().get_max_life(),
game.get_player().get_experience(), game.get_player().get_experience(),
game.get_player().get_gold(), game.get_player().get_gold(),
game.get_player().get_immutable_position().get_level())) game.get_player().get_immutable_position().get_level()))
.white() .block(block).wrap(Wrap { trim: true }),
.on_blue(),
stats_area, stats_area,
); );
let messages_area = Rect {
x: area.x + 50,
y: area.y + 15,
width: 30,
height: 10,
};
// Display the latest messages from the game to the user
let block = Block::default()
.title(Title::from(" messages ").alignment(Alignment::Center).position(Position::Top))
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Blue));
let paragraph1 = if game.messages.is_empty() { "".to_string() } else { format!("> {}", game.messages.join("\n> ")) };
frame.render_widget(
Paragraph::new(paragraph1).block(block).wrap(Wrap { trim: true }),
messages_area,
);
})?; })?;
if event::poll(std::time::Duration::from_millis(FRAME_LENGTH))? { if event::poll(std::time::Duration::from_millis(FRAME_LENGTH))? {
if let event::Event::Key(key) = event::read()? { if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('v') {
game.messages.insert(0, format!("You are playing version '{}'.", env!("GIT_HASH")).to_string());
}
// disabling the d key (disable fog of war) in release builds!
#[cfg(debug_assertions)]
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('d') {
game.messages.insert(0, "toggle fog of war!".to_string());
game.toggle_fog_of_war();
}
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break; break;
} }
@@ -140,7 +180,7 @@ fn main() -> Result<()> {
area.width = 40; area.width = 40;
area.height = 20; area.height = 20;
let block = Block::default() let block = Block::default()
.title("Game ended") .title(Title::from(" Game ended ").alignment(Alignment::Center).position(Position::Top))
.title(Title::from("Press `q` to quit!").position(Position::Bottom)) .title(Title::from("Press `q` to quit!").position(Position::Bottom))
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::White)) .border_style(Style::default().fg(Color::White))
@@ -151,13 +191,13 @@ fn main() -> Result<()> {
"Quitting is for cowards! You'll better try again!".to_string() "Quitting is for cowards! You'll better try again!".to_string()
} }
GameState::LOST => { GameState::LOST => {
"Sorry, you died in the dungeon. Better luck next time!".to_string() format!("Sorry, you died in the dungeon. Better luck next time!\nLast message:\n{}", game.messages[0]).to_string()
} }
GameState::WON => { GameState::WON => {
"Congratulation! You mastered your way through the dungeon and won the game.".to_string() "Congratulation! You mastered your way through the dungeon and won the game.".to_string()
} }
}; };
text += format!("\nYou gained {} experience.", game.get_player().get_experience()).as_str(); text += format!("\n\nYou gained {} experience.", game.get_player().get_experience()).as_str();
text += format!("\nYou collected {} gold.", game.get_player().get_gold()).as_str(); text += format!("\nYou collected {} gold.", game.get_player().get_gold()).as_str();
text += format!("\nYou played {} seconds.", playtime.as_secs()).as_str(); text += format!("\nYou played {} seconds.", playtime.as_secs()).as_str();
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
@@ -171,7 +211,6 @@ fn main() -> Result<()> {
} }
} }
} }
stdout().execute(LeaveAlternateScreen)?; stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?; disable_raw_mode()?;
Ok(()) Ok(())

View File

@@ -1,14 +1,22 @@
use ratatui::prelude::Color; use ratatui::prelude::Color;
use crate::dungeon_slayer::MonsterStats;
use crate::level::Level;
use crate::position::{HasPosition, Position}; use crate::position::{HasPosition, Position};
use rand::Rng;
pub trait Monster: HasPosition { pub trait Monster: HasPosition {
fn get_name(&self) -> &str;
fn is_dead(&self) -> bool; fn is_dead(&self) -> bool;
fn get_representation(&self) -> (&str, Color); fn get_representation(&self) -> (&str, Color);
fn decrease_life(&mut self, by: usize); fn decrease_life(&mut self, by: usize);
// fn get_immutable_position(&self) -> &Position; // fn get_immutable_position(&self) -> &Position;
fn get_experience_gain(&self) -> usize; fn get_experience_gain(&self) -> usize;
fn get_ticks_between_steps(&self) -> u128; fn get_ticks_between_steps(&self) -> u128;
fn get_defense(&self) -> u8;
fn get_hit(&self) -> u8;
fn get_next_move(&self, level: &Level, player_pos: &Position) -> (i16, i16);
#[cfg(test)] #[cfg(test)]
fn get_life(&self) -> usize; fn get_life(&self) -> usize;
} }
@@ -16,6 +24,7 @@ pub trait Monster: HasPosition {
macro_rules! default_monster { macro_rules! default_monster {
($($t:ty),+ $(,)?) => ($( ($($t:ty),+ $(,)?) => ($(
impl Monster for $t { impl Monster for $t {
fn get_name(&self) -> &str { &self.name }
fn is_dead(&self) -> bool { self.life <= 0 } fn is_dead(&self) -> bool { self.life <= 0 }
fn get_experience_gain(&self) -> usize { self.experience_gain } fn get_experience_gain(&self) -> usize { self.experience_gain }
fn get_representation(&self) -> (&str, Color) { (&self.symbol, self.color) } fn get_representation(&self) -> (&str, Color) { (&self.symbol, self.color) }
@@ -23,7 +32,11 @@ macro_rules! default_monster {
self.life = self.life.saturating_sub(by); self.life = self.life.saturating_sub(by);
} }
fn get_ticks_between_steps(&self) -> u128 { self.ticks_between_steps } fn get_ticks_between_steps(&self) -> u128 { self.ticks_between_steps }
fn get_defense(&self) -> u8 { self.monster_stats.defense }
fn get_hit(&self) -> u8 { self.monster_stats.hit }
fn get_next_move(&self, level: &Level, player_pos: &Position) -> (i16, i16) {
self.calc_move(level, player_pos)
}
#[cfg(test)] #[cfg(test)]
fn get_life(&self) -> usize { self.life } fn get_life(&self) -> usize { self.life }
} }
@@ -38,47 +51,180 @@ macro_rules! default_monster {
)+) )+)
} }
pub struct Rat { /// Lower Daemon
/// Page: 107
/// GH: 1
pub struct LowerDaemon {
name: String,
life: usize, life: usize,
position: Position, position: Position,
symbol: String, symbol: String,
color: Color, color: Color,
experience_gain: usize, experience_gain: usize,
ticks_between_steps: u128, ticks_between_steps: u128,
monster_stats: MonsterStats,
}
impl crate::monster::LowerDaemon {
pub fn new_with_position(position: Position) -> Self {
let monster_stats = MonsterStats {
body: 5,
agility: 5,
mind: 5,
strength: 2,
toughness: 2,
movement: 2,
dexterity: 2,
wisdom: 2,
aura: 2,
max_life: 9,
defense: 9,
initiative: 7,
walk: 3,
hit: 8,
shoot: 0,
cast: 0,
targeted_cast: 0,
};
Self {
name: "lower daemon".to_string(),
life: monster_stats.max_life as usize,
position,
symbol: String::from("d"),
color: Color::Black,
experience_gain: 104,
ticks_between_steps: 5,
monster_stats,
}
}
fn calc_move(&self, _level: &Level, player_pos: &Position) -> (i16, i16) {
let d = self.position.distance(player_pos);
if d == 1 {
return self.position.get_direction(player_pos);
}
match rand::thread_rng().gen_range(0..5) {
1 => { (1, 0) }
2 => { (-1, 0) }
3 => { (0, 1) }
4 => { (0, -1) }
_ => { (0, 0) }
}
}
}
default_monster!(LowerDaemon);
/// Rat
/// Page: 119
/// GH: 1
pub struct Rat {
name: String,
life: usize,
position: Position,
symbol: String,
color: Color,
experience_gain: usize,
ticks_between_steps: u128,
monster_stats: MonsterStats,
} }
impl Rat { impl Rat {
pub fn new_with_position(position: Position) -> Self { pub fn new_with_position(position: Position) -> Self {
let monster_stats = MonsterStats {
body: 2,
agility: 4,
mind: 1,
strength: 1,
toughness: 0,
movement: 2,
dexterity: 0,
wisdom: 0,
aura: 0,
max_life: 3,
defense: 2,
initiative: 6,
walk: 3,
hit: 4,
shoot: 0,
cast: 0,
targeted_cast: 0,
};
Self { Self {
life: 2, name: "rat".to_string(),
life: monster_stats.max_life as usize,
position, position,
symbol: String::from("R"), symbol: String::from("R"),
color: Color::Black, color: Color::Black,
experience_gain: 5, experience_gain: 26,
ticks_between_steps: 5, ticks_between_steps: 5,
monster_stats,
}
}
fn calc_move(&self, _level: &Level, _player_pos: &Position) -> (i16, i16) {
match rand::thread_rng().gen_range(0..5) {
1 => { (1, 0) }
2 => { (-1, 0) }
3 => { (0, 1) }
4 => { (0, -1) }
_ => { (0, 0) }
} }
} }
} }
default_monster!(Rat); default_monster!(Rat);
/// Orc
/// Page: 118
/// GH: 2
pub struct Orc { pub struct Orc {
name: String,
life: usize, life: usize,
position: Position, position: Position,
symbol: String, symbol: String,
color: Color, color: Color,
experience_gain: usize, experience_gain: usize,
ticks_between_steps: u128, ticks_between_steps: u128,
monster_stats: MonsterStats,
} }
impl Orc { impl Orc {
pub fn new_with_position(position: Position) -> Self { pub fn new_with_position(position: Position) -> Self {
let monster_stats = MonsterStats {
body: 10,
agility: 6,
mind: 2,
strength: 2,
toughness: 3,
movement: 0,
dexterity: 3,
wisdom: 1,
aura: 0,
max_life: 23,
defense: 14,
initiative: 1,
walk: 4,
hit: 13,
shoot: 10,
cast: 0,
targeted_cast: 0,
};
Self { Self {
life: 4, name: "orc".to_string(),
life: monster_stats.max_life as usize,
position, position,
symbol: String::from("O"), symbol: String::from("O"),
color: Color::DarkGray, color: Color::DarkGray,
experience_gain: 10, experience_gain: 63,
ticks_between_steps: 10, ticks_between_steps: 10,
monster_stats,
}
}
fn calc_move(&self, _level: &Level, _player_pos: &Position) -> (i16, i16) {
match rand::thread_rng().gen_range(0..5) {
1 => { (1, 0) }
2 => { (-1, 0) }
3 => { (0, 1) }
4 => { (0, -1) }
_ => { (0, 0) }
} }
} }
} }
@@ -88,7 +234,7 @@ default_monster!(Orc);
#[test] #[test]
fn monsters_can_move() { fn monsters_can_move() {
let mut m = Rat::new_with_position(Position::new(0,0,0)); let mut m = Rat::new_with_position(Position::new(0, 0, 0));
assert_eq!(m.get_position(), &Position::new(0, 0, 0)); assert_eq!(m.get_position(), &Position::new(0, 0, 0));
m.get_position().change(1, 2); m.get_position().change(1, 2);
assert_eq!(m.get_position(), &Position::new(0, 1, 2)); assert_eq!(m.get_position(), &Position::new(0, 1, 2));
@@ -101,12 +247,18 @@ fn monsters_can_move() {
#[test] #[test]
fn monsters_can_die() { fn monsters_can_die() {
let mut m = Rat::new_with_position(Position::new(0,0,0)); let mut m = Rat::new_with_position(Position::new(0, 0, 0));
assert_eq!(m.get_life(), 2); assert_eq!(m.get_life(), 3);
assert_eq!(m.is_dead(), false); assert_eq!(m.is_dead(), false);
m.decrease_life(1); m.decrease_life(1);
assert_eq!(m.get_life(), 1); assert_eq!(m.get_life(), 2);
m.decrease_life(2); m.decrease_life(3);
assert_eq!(m.get_life(), 0); assert_eq!(m.get_life(), 0);
assert_eq!(m.is_dead(), true); assert_eq!(m.is_dead(), true);
}
#[test]
fn test_rat_values() {
let m = Rat::new_with_position(Position::new(0, 0, 0));
assert_eq!(m.monster_stats.max_life, 3);
} }

View File

@@ -1,40 +1,46 @@
use std::cmp::{max, min}; use std::cmp::{max, min};
use crate::dungeon_slayer::PlayerStats;
use crate::position::{HasPosition, Position}; use crate::position::{HasPosition, Position};
pub struct Player { pub struct Player {
name: String, name: String,
position: Position, position: Position,
life: i16, life: i16,
max_life: i16,
gold: usize, gold: usize,
experience: usize, experience: usize,
pub(crate) player_stats: PlayerStats,
} }
impl Player { impl Player {
pub fn new(name: &str, max_life: i16) -> Player { pub fn new(name: &str, player_stats: PlayerStats) -> Player {
Player { Player {
name: name.to_string(), name: name.to_string(),
position: Position::new(0, 0, 0), position: Position::new(0, 0, 0),
life: max_life, life: player_stats.get_max_life() as i16,
max_life,
gold: 0, gold: 0,
experience: 0, experience: 0,
player_stats,
} }
} }
pub fn get_name(&self) -> String { pub fn get_name(&self) -> String {
return self.name.clone(); return self.name.clone();
} }
pub fn change_life(&mut self, by: i16) { pub fn decrease_life(&mut self, by: u8) {
self.life = max(0, min(self.max_life, self.life + by)); self.life = max(0, self.life.saturating_sub(by as i16));
}
pub fn increase_life(&mut self, by: u8) {
self.life = min(self.life.saturating_add(by as i16), self.get_max_life());
} }
/// returns true if the player is dead (life <= 0)
pub fn get_life(&self) -> i16 { pub fn get_life(&self) -> i16 {
self.life self.life
} }
/// returns true if the player is dead (life <= 0)
pub fn is_dead(&self) -> bool { self.life <= 0 } pub fn is_dead(&self) -> bool { self.life <= 0 }
/// returns true if the player's life is at maximum
pub fn is_healthy(&self) -> bool { self.life == self.get_max_life() }
pub fn get_max_life(&self) -> i16 { pub fn get_max_life(&self) -> i16 {
self.max_life self.player_stats.get_max_life() as i16
} }
/// add the given amount to the players gold stash /// add the given amount to the players gold stash
@@ -63,16 +69,37 @@ fn test_get_name() {
name: "Teddy Tester".to_string(), name: "Teddy Tester".to_string(),
position: Position::new(0, 1, 1), position: Position::new(0, 1, 1),
life: 5, life: 5,
max_life: 10,
gold: 0, gold: 0,
experience: 0, experience: 0,
player_stats: PlayerStats {
body: 0,
agility: 0,
mind: 0,
strength: 0,
toughness: 0,
movement: 0,
dexterity: 0,
wisdom: 0,
aura: 0,
},
}; };
assert_eq!(p.get_name(), "Teddy Tester"); assert_eq!(p.get_name(), "Teddy Tester");
} }
#[test] #[test]
fn test_can_receive_gold() { fn test_can_receive_gold() {
let mut p = Player::new("Teddy Tester", 10); let player_stats = PlayerStats {
body: 8,
agility: 8,
mind: 4,
strength: 3,
toughness: 3,
movement: 1,
dexterity: 1,
wisdom: 0,
aura: 0,
};
let mut p = Player::new("Teddy Tester", player_stats);
assert_eq!(p.get_gold(), 0); assert_eq!(p.get_gold(), 0);
p.retrieve_gold(13); p.retrieve_gold(13);
@@ -86,22 +113,43 @@ fn test_change_life() {
name: "Teddy Tester".to_string(), name: "Teddy Tester".to_string(),
position: Position::new(0, 1, 1), position: Position::new(0, 1, 1),
life: 5, life: 5,
max_life: 10,
gold: 0, gold: 0,
experience: 0, experience: 0,
player_stats: PlayerStats {
body: 0,
agility: 0,
mind: 0,
strength: 0,
toughness: 0,
movement: 0,
dexterity: 0,
wisdom: 0,
aura: 0,
},
}; };
assert_eq!(p.get_life(), 5); assert_eq!(p.get_life(), 5);
p.change_life(-2); p.decrease_life(2);
assert_eq!(p.get_life(), 3); assert_eq!(p.get_life(), 3);
p.change_life(10); p.increase_life(10);
assert_eq!(p.get_life(), 10); assert_eq!(p.get_life(), 10);
p.change_life(-12); p.decrease_life(12);
assert_eq!(p.get_life(), 0); assert_eq!(p.get_life(), 0);
} }
#[test] #[test]
fn player_can_move() { fn player_can_move() {
let mut p = Player::new("Teddy Tester", 10); let player_stats = PlayerStats {
body: 8,
agility: 8,
mind: 4,
strength: 3,
toughness: 3,
movement: 1,
dexterity: 1,
wisdom: 0,
aura: 0,
};
let mut p = Player::new("Teddy Tester", player_stats);
assert_eq!(p.get_position(), &Position::new(0, 0, 0)); assert_eq!(p.get_position(), &Position::new(0, 0, 0));
p.get_position().change(1, 2); p.get_position().change(1, 2);
assert_eq!(p.get_position(), &Position::new(0, 1, 2)); assert_eq!(p.get_position(), &Position::new(0, 1, 2));
@@ -119,9 +167,19 @@ fn test_max_life() {
name: "Teddy Tester".to_string(), name: "Teddy Tester".to_string(),
position: Position::new(0, 1, 1), position: Position::new(0, 1, 1),
life: 5, life: 5,
max_life: 10,
gold: 0, gold: 0,
experience: 0, experience: 0,
player_stats: PlayerStats {
body: 0,
agility: 0,
mind: 0,
strength: 0,
toughness: 0,
movement: 0,
dexterity: 0,
wisdom: 0,
aura: 0,
},
}; };
assert_eq!(p.get_max_life(), 10); assert_eq!(p.get_max_life(), 10);
} }

View File

@@ -1,4 +1,4 @@
use std::cmp::max; use std::cmp::{max, min};
/// describes an character (PC or NPC) in the dungeon that has a position. /// describes an character (PC or NPC) in the dungeon that has a position.
pub trait HasPosition { pub trait HasPosition {
@@ -43,6 +43,28 @@ impl Position {
} }
pub fn get_y(&self) -> usize { self.y } pub fn get_y(&self) -> usize { self.y }
/// calculate the distance to the other position using the manhattan metric
pub fn distance(&self, other: &Self) -> usize {
self.x.abs_diff(other.x) + self.y.abs_diff(other.y)
}
/// get the direction (step size 1) towards the other position
pub fn get_direction(&self, other: &Self) -> (i16, i16) {
(
max(min(other.x as i16 - self.x as i16, 1), -1),
max(min(other.y as i16 - self.y as i16, 1), -1)
)
}
}
#[test]
fn test_get_direction() {
let p1 = Position::new(0, 10, 10);
let p2 = Position::new(0, 8, 8);
assert_eq!(p1.get_direction(&p2), (-1, -1));
let p2 = Position::new(0, 11, 10);
assert_eq!(p1.get_direction(&p2), (1, 0));
} }
#[test] #[test]