37 Commits

Author SHA1 Message Date
530067f743 fix formatting 2024-11-25 21:20:36 +01:00
94c787d8c7 rename monster.rs to monsters.rs 2024-11-25 07:20:03 +01:00
9776482922 version bump 2024-11-24 13:30:31 +01:00
fb69c932a8 adjust background colors 2024-11-24 13:30:10 +01:00
62e13ace4c add defense attribute to player and monster 2024-11-24 13:24:39 +01:00
51123ecd8e version bump and index generation 2024-11-07 09:57:32 +01:00
4d77265b2f remove debug output 2024-11-07 09:47:49 +01:00
c91d57f049 next try 2024-11-07 09:46:15 +01:00
8f151836bd next try 2024-11-07 09:41:40 +01:00
9d862011a7 testing 2024-11-07 09:37:22 +01:00
d068620c3f more tuning 2024-11-07 09:35:30 +01:00
6660c11306 more debug 2024-11-07 09:29:24 +01:00
1d8896d1ad debugging 2024-11-07 09:27:31 +01:00
252c1c244d make key secure in container 2024-11-07 09:24:19 +01:00
ef3a6026f5 minor fixes 2024-11-07 09:22:19 +01:00
542616cb0b version bump 2024-11-07 09:15:44 +01:00
9a6c8f5116 try to auto publish to website 2024-11-07 09:15:24 +01:00
d82e0d2d48 version updates 2024-11-06 14:33:11 +01:00
a542e4b1c0 add mechanism for level gain 2024-11-06 14:29:30 +01:00
3249529f94 cleanup 2024-11-06 09:22:22 +01:00
72f7be2ed8 more work on level generators 2024-11-05 12:02:45 +01:00
5311f56ca0 extract minimal room size as constant 2024-11-03 10:32:04 +01:00
df80dfdd8a introduce custom type for the graph 2024-11-01 13:08:02 +01:00
8b23c565b8 make connections more variable 2024-11-01 12:06:50 +01:00
789a41cb3f make connections more variable 2024-11-01 12:02:11 +01:00
d42d8a12b4 make spiders more visible 2024-11-01 08:49:12 +01:00
8ee142586b make monsters visible 2024-10-31 22:39:11 +01:00
1878959e69 Clippy 2024-10-31 22:36:23 +01:00
8bae1c7668 formatted 2024-10-31 22:34:35 +01:00
3096386ad1 improve paths 2024-10-31 21:52:04 +01:00
f5d257e826 more work on generator and tests 2024-10-31 15:20:39 +01:00
a82a847ecd more tests 2024-10-31 09:58:15 +01:00
50d98bfb4d work on level generation including tests 2024-10-31 07:48:22 +01:00
c429784775 run tests on all branches 2024-10-30 13:06:26 +01:00
e96660cea0 redo some colors 2024-10-30 13:01:28 +01:00
c6492c28c2 work on level generator 2024-10-30 08:56:45 +01:00
7f288dbcd1 first version of the level new generator
monsters and artefact are missing
roomsize is fixed
2024-10-29 06:48:45 +01:00
18 changed files with 1489 additions and 424 deletions

View File

@@ -0,0 +1,16 @@
name: publish package
on:
push:
branches-ignore:
- main
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: run tests
run: |
cd ${{ gitea.workspace }}
cargo test

View File

@@ -22,3 +22,7 @@ jobs:
ls -al target/debian ls -al target/debian
UPLOAD_FILE=$(ls target/debian/*.deb) UPLOAD_FILE=$(ls target/debian/*.deb)
curl --user jlusiardi:${{ secrets.UPLOAD_TOKEN }} --upload-file ${UPLOAD_FILE} https://gitea.intern.haus-in-hirschaid.de/api/packages/jlusiardi/debian/pool/bionic/main/upload curl --user jlusiardi:${{ secrets.UPLOAD_TOKEN }} --upload-file ${UPLOAD_FILE} https://gitea.intern.haus-in-hirschaid.de/api/packages/jlusiardi/debian/pool/bionic/main/upload
echo -e "${{ secrets.DEPLOY_PRIVATE_KEY }}" > /tmp/ssh_key
chmod 0600 /tmp/ssh_key
scp -o StrictHostKeyChecking=no -i /tmp/ssh_key ${UPLOAD_FILE} jlusiardi@april2023.lusiardi.de:/var/www/p17.lusiardi.de/el_diabolo
ssh -o StrictHostKeyChecking=no -i /tmp/ssh_key jlusiardi@april2023.lusiardi.de /var/www/p17.lusiardi.de/el_diabolo/generate_index.sh

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target /target
*.profraw

24
Cargo.lock generated
View File

@@ -147,7 +147,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "el_diabolo" name = "el_diabolo"
version = "0.1.0" version = "0.2.3"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"macros", "macros",
@@ -234,6 +234,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]] [[package]]
name = "instability" name = "instability"
version = "0.3.2" version = "0.3.2"
@@ -429,23 +435,23 @@ dependencies = [
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.28.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"cassowary", "cassowary",
"compact_str", "compact_str",
"crossterm", "crossterm",
"indoc",
"instability", "instability",
"itertools", "itertools",
"lru", "lru",
"paste", "paste",
"strum", "strum",
"strum_macros",
"unicode-segmentation", "unicode-segmentation",
"unicode-truncate", "unicode-truncate",
"unicode-width", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@@ -598,7 +604,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [ dependencies = [
"itertools", "itertools",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.1.14",
] ]
[[package]] [[package]]
@@ -607,6 +613,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@@ -3,13 +3,13 @@ members = ["macros"]
[package] [package]
name = "el_diabolo" name = "el_diabolo"
version = "0.1.0" version = "0.2.3"
edition = "2021" 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.28.1" ratatui = "0.29.0"
crossterm = "0.28.1" crossterm = "0.28.1"
rand = "0.8.5" rand = "0.8.5"
petgraph = "0.6.5" petgraph = "0.6.5"

10
coverage.md Normal file
View File

@@ -0,0 +1,10 @@
```
cargo install grcov
rustup component add llvm-tools-preview
```
```
RUSTFLAGS="-Cinstrument-coverage" cargo clean
RUSTFLAGS="-Cinstrument-coverage" cargo test
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
```

View File

@@ -3,7 +3,6 @@ extern crate proc_macro;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
#[proc_macro_derive(CreateMonsters)] #[proc_macro_derive(CreateMonsters)]
pub fn create_monsters(input: TokenStream) -> TokenStream { pub fn create_monsters(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::File); let input = syn::parse_macro_input!(input as syn::File);
@@ -25,6 +24,7 @@ pub fn create_monsters(input: TokenStream) -> TokenStream {
experience_gain: usize, experience_gain: usize,
ticks_between_steps: u128, ticks_between_steps: u128,
damage_range: RangeInclusive<usize>, damage_range: RangeInclusive<usize>,
defense: usize,
} }
impl Monster for #variant_name { impl Monster for #variant_name {
fn get_name(&self) -> &str { &self.name } fn get_name(&self) -> &str { &self.name }
@@ -36,7 +36,7 @@ pub fn create_monsters(input: TokenStream) -> TokenStream {
} }
fn get_ticks_between_steps(&self) -> u128 { self.ticks_between_steps } fn get_ticks_between_steps(&self) -> u128 { self.ticks_between_steps }
fn damage(&self) -> usize { rand::thread_rng().gen_range(self.damage_range.clone()) } fn damage(&self) -> usize { rand::thread_rng().gen_range(self.damage_range.clone()) }
fn defense(&self) -> usize { self.defense }
#[cfg(test)] #[cfg(test)]
fn get_life(&self) -> usize { self.life } fn get_life(&self) -> usize { self.life }
} }

View File

@@ -39,7 +39,7 @@ impl Chest {
impl Artifact for Chest { impl Artifact for Chest {
fn get_representation(&self) -> (&str, Color) { fn get_representation(&self) -> (&str, Color) {
("C", Color::Blue) ("C", Color::Cyan)
} }
fn get_immutable_position(&self) -> &Position { fn get_immutable_position(&self) -> &Position {

View File

@@ -1,19 +1,25 @@
use std::{collections::HashMap, ops::RangeInclusive}; use std::{collections::HashMap, ops::RangeInclusive};
use crate::monster::MonsterTypes; use crate::{monsters::MonsterTypes, room::RoomType};
/// the number of rooms in vertical direction /// the number of rooms in vertical direction
pub const ROOMS_VERTICAL: usize = 7; pub const ROOMS_HORIZONTAL: usize = 8;
/// the number of rooms in horizontal direction /// the number of rooms in horizontal direction
pub const ROOMS_HORIZONTAL: usize = 7; pub const ROOMS_VERTICAL: usize = 7;
/// the width of a room in the grid of rooms (number of characters) /// the width of a room in the grid of rooms (number of characters)
pub const ROOM_WIDTH: usize = 9; pub const ROOM_WIDTH: usize = 9;
/// the height of a room in the grid of rooms (number of characters) /// the height of a room in the grid of rooms (number of characters)
pub const ROOM_HEIGHT: usize = 6; pub const ROOM_HEIGHT: usize = 6;
// the minmal width of a room
pub const ROOM_MIN_WIDTH: usize = 4;
// the minmal height of a room
pub const ROOM_MIN_HEIGHT: usize = 4;
/// How many levels does the dungeon have? /// How many levels does the dungeon have?
pub const LEVELS: usize = 3; pub const LEVELS: usize = 4;
/// 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;
@@ -24,9 +30,9 @@ pub const MIN_WIDTH: u16 = 120;
pub const MIN_HEIGHT: u16 = LEVEL_HEIGHT as u16; pub const MIN_HEIGHT: u16 = LEVEL_HEIGHT as u16;
/// the calculated width of a level /// the calculated width of a level
pub const LEVEL_WIDTH: usize = 1 + ROOMS_VERTICAL * ROOM_WIDTH; pub const LEVEL_WIDTH: usize = 1 + ROOMS_HORIZONTAL * ROOM_WIDTH;
/// the calculated height of a level /// the calculated height of a level
pub const LEVEL_HEIGHT: usize = 1 + ROOMS_HORIZONTAL * ROOM_HEIGHT; pub const LEVEL_HEIGHT: usize = 1 + ROOMS_VERTICAL * ROOM_HEIGHT;
pub fn get_monsters_per_level() -> Vec<HashMap<MonsterTypes, std::ops::RangeInclusive<u8>>> { pub fn get_monsters_per_level() -> Vec<HashMap<MonsterTypes, std::ops::RangeInclusive<u8>>> {
let tmp = [ let tmp = [
@@ -35,10 +41,24 @@ pub fn get_monsters_per_level() -> Vec<HashMap<MonsterTypes, std::ops::RangeIncl
// level 2 // level 2
vec![(MonsterTypes::Rat, 50), (MonsterTypes::Snake, 50)], vec![(MonsterTypes::Rat, 50), (MonsterTypes::Snake, 50)],
// level 3 // level 3
vec![(MonsterTypes::Orc, 34), (MonsterTypes::Skeleton, 33), (MonsterTypes::Snake, 33)], vec![
(MonsterTypes::Orc, 34),
(MonsterTypes::Skeleton, 33),
(MonsterTypes::Snake, 33),
],
// level 4
vec![
(MonsterTypes::Orc, 34),
(MonsterTypes::Skeleton, 33),
(MonsterTypes::Snake, 33),
],
]; ];
if tmp.len() != LEVELS { if tmp.len() < LEVELS {
panic!("Only {} monster sets defined for {} levels!", tmp.len(), LEVELS); panic!(
"Only {} monster sets defined for {} levels!",
tmp.len(),
LEVELS
);
} }
let mut result: Vec<HashMap<MonsterTypes, std::ops::RangeInclusive<u8>>> = vec![]; let mut result: Vec<HashMap<MonsterTypes, std::ops::RangeInclusive<u8>>> = vec![];
for (idx, level) in tmp.iter().enumerate() { for (idx, level) in tmp.iter().enumerate() {
@@ -47,10 +67,69 @@ pub fn get_monsters_per_level() -> Vec<HashMap<MonsterTypes, std::ops::RangeIncl
for monster in level { for monster in level {
map.insert(monster.0, RangeInclusive::new(sum + 1, sum + monster.1)); map.insert(monster.0, RangeInclusive::new(sum + 1, sum + monster.1));
sum += monster.1; sum += monster.1;
} }
if sum != 100 { if sum != 100 {
panic!("all percentages must add to 100 (was {}) per level, error in level {}!", sum, idx+1); panic!(
"all percentages must add to 100 (was {}) per level, error in level {}!",
sum,
idx + 1
);
}
result.push(map);
}
result
}
pub fn get_room_type_per_level() -> Vec<HashMap<RoomType, std::ops::RangeInclusive<u8>>> {
let tmp = [
// level 1
vec![
(RoomType::EmptyRoom, 50),
(RoomType::ArtifactRoom, 10),
(RoomType::MonsterRoom, 20),
(RoomType::BasicRoom, 20),
],
// level 2
vec![
(RoomType::EmptyRoom, 50),
(RoomType::BasicRoom, 25),
(RoomType::MonsterRoom, 12),
(RoomType::ArtifactRoom, 13),
],
// level 3
vec![
(RoomType::EmptyRoom, 50),
(RoomType::BasicRoom, 25),
(RoomType::MonsterRoom, 25),
],
// level 4
vec![
(RoomType::BasicRoom, 33),
(RoomType::MonsterRoom, 33),
(RoomType::ArtifactRoom, 34),
],
];
if tmp.len() < LEVELS {
panic!(
"Only {} room sets defined for {} levels!",
tmp.len(),
LEVELS
);
}
let mut result: Vec<HashMap<RoomType, std::ops::RangeInclusive<u8>>> = vec![];
for (idx, level) in tmp.iter().enumerate() {
let mut sum = 0;
let mut map: HashMap<RoomType, RangeInclusive<u8>> = HashMap::new();
for room in level {
map.insert(room.0, RangeInclusive::new(sum + 1, sum + room.1));
sum += room.1;
}
if sum != 100 {
panic!(
"all percentages must add to 100 (was {}) per level, error in level {}!",
sum,
idx + 1
);
} }
result.push(map); result.push(map);
} }

View File

@@ -164,10 +164,10 @@ impl Game {
match m { match m {
None => {} None => {}
Some(m) => { Some(m) => {
let player_dmg = self.player.damage(); let player_dmg = self.player.damage().saturating_sub(m.defense());
m.decrease_life(player_dmg); m.decrease_life(player_dmg);
if m.is_dead() { if m.is_dead() {
self.player.gain_experience(m.get_experience_gain()); let level_up: bool = self.player.gain_experience(m.get_experience_gain());
self.messages.insert( self.messages.insert(
0, 0,
format!( format!(
@@ -177,13 +177,23 @@ impl Game {
) )
.to_string(), .to_string(),
); );
if level_up {
self.messages.insert(
0,
format!(
"you reach a new experience level, it's now {}",
self.player.get_experience_level(),
)
.to_string(),
);
}
} else { } else {
self.messages.insert( self.messages.insert(
0, 0,
format!("you hit {} for {} damage.", m.get_name(), player_dmg).to_string(), format!("you hit {} for {} damage.", m.get_name(), player_dmg).to_string(),
); );
let monster_dmg = m.damage() as i16; let monster_dmg = m.damage().saturating_sub(self.player.defense());
self.player.change_life(-monster_dmg); self.player.change_life(-(monster_dmg as i16));
self.messages.insert( self.messages.insert(
0, 0,

View File

@@ -8,9 +8,9 @@ use crate::artifacts::Artifact;
use crate::artifacts::{Chest, Potion}; use crate::artifacts::{Chest, Potion};
use crate::constants::LEVEL_HEIGHT; use crate::constants::LEVEL_HEIGHT;
use crate::constants::LEVEL_WIDTH; use crate::constants::LEVEL_WIDTH;
use crate::monster::Monster; use crate::monsters::Monster;
#[cfg(test)] #[cfg(test)]
use crate::monster::{Orc, Rat}; use crate::monsters::{Orc, Rat};
use crate::player::Player; use crate::player::Player;
use crate::position::HasPosition; use crate::position::HasPosition;
use crate::position::Position; use crate::position::Position;
@@ -57,6 +57,7 @@ impl Level {
return (None, None, None); return (None, None, None);
} }
if !self.discovered[x][y] { if !self.discovered[x][y] {
// #[cfg(test)]
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);
@@ -146,8 +147,21 @@ impl Level {
if player.get_immutable_position().get_x() == new_x if player.get_immutable_position().get_x() == new_x
&& player.get_immutable_position().get_y() == new_y && player.get_immutable_position().get_y() == new_y
{ {
let monster_dmg = self.monsters[index].damage() as i16; let monster_dmg = self.monsters[index]
player.change_life(-monster_dmg); .damage()
.saturating_sub(player.defense());
player.change_life(-(monster_dmg as i16));
if player.is_dead() {
messages.insert(
0,
format!(
"{} hits you for {} damage and kills you.",
self.monsters[index].get_name(),
monster_dmg
)
.to_string(),
);
} else {
messages.insert( messages.insert(
0, 0,
format!( format!(
@@ -157,12 +171,16 @@ impl Level {
) )
.to_string(), .to_string(),
); );
// player not dead => monster can occupy new position
let player_dmg = player.damage(); self.monsters[index].get_position().change(-dx, -dy);
// player fights back
let player_dmg = player
.damage()
.saturating_sub(self.monsters[index].defense());
self.monsters[index].decrease_life(player_dmg); self.monsters[index].decrease_life(player_dmg);
if self.monsters[index].is_dead() {
// if the attack did not kill the opponent, back down let level_up: bool = player
if !player.is_dead() { .gain_experience(self.monsters[index].get_experience_gain());
messages.insert( messages.insert(
0, 0,
format!( format!(
@@ -172,7 +190,16 @@ impl Level {
) )
.to_string(), .to_string(),
); );
self.monsters[index].get_position().change(-dx, -dy); if level_up {
messages.insert(
0,
format!(
"you reach a new experience level, it's now {}",
player.get_experience_level(),
)
.to_string(),
);
}
} else { } else {
messages.insert( messages.insert(
0, 0,
@@ -185,6 +212,7 @@ impl Level {
); );
} }
} }
}
break; break;
} }
} }

File diff suppressed because it is too large Load Diff

30
src/level_ladder.rs Normal file
View File

@@ -0,0 +1,30 @@
pub struct LevelLadder {
curr: usize,
next: usize,
}
impl Iterator for LevelLadder {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
let current = self.curr;
self.curr = self.next;
self.next += current;
// Since there's no endpoint to a Fibonacci sequence, the `Iterator`
// will never return `None`, and `Some` is always returned.
Some(current * 30)
}
}
// Returns a Fibonacci sequence generator
pub fn get_level_ladder() -> LevelLadder {
LevelLadder { curr: 1, next: 2 }
}
#[test]
fn test_level_ladder() {
let mut iter = get_level_ladder();
assert_eq!(iter.next(), Some(30));
}

View File

@@ -1,13 +1,14 @@
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::style::Color; use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{StatefulWidget, Widget}; use ratatui::widgets::{StatefulWidget, Widget};
use crate::game::Game; use crate::game::Game;
use crate::level::StructureElement; use crate::level::StructureElement;
use crate::position::HasPosition; use crate::position::HasPosition;
const FG_BROWN: Color = Color::Rgb(186, 74, 0); const FG_BROWN: Color = Color::Rgb(140, 34, 0);
const BACKGROUND: Color = Color::Black;
pub struct LevelWidget {} pub struct LevelWidget {}
@@ -15,6 +16,13 @@ impl LevelWidget {
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[(x, y)].set_symbol(symbol).set_bg(bg).set_fg(fg); buf[(x, y)].set_symbol(symbol).set_bg(bg).set_fg(fg);
} }
fn set_bold_cell(&self, buf: &mut Buffer, x: u16, y: u16, symbol: &str, fg: Color, bg: Color) {
buf[(x, y)]
.set_symbol(symbol)
.set_bg(bg)
.set_fg(fg)
.set_style(Style::new().add_modifier(Modifier::BOLD));
}
} }
impl StatefulWidget for LevelWidget { impl StatefulWidget for LevelWidget {
@@ -39,36 +47,36 @@ impl StatefulWidget for LevelWidget {
(Some(structure_element), None, None) => { (Some(structure_element), None, None) => {
match structure_element { match structure_element {
StructureElement::Start => { StructureElement::Start => {
self.set_cell(buf, x, y, "α", Color::Black, Color::Gray); self.set_cell(buf, x, y, "α", Color::White, BACKGROUND);
} }
StructureElement::End => { StructureElement::End => {
self.set_cell(buf, x, y, "Ω", Color::Black, Color::Gray); self.set_cell(buf, x, y, "Ω", Color::White, BACKGROUND);
} }
StructureElement::Wall => { StructureElement::Wall => {
// TODO add fancy walls with https://en.wikipedia.org/wiki/Box-drawing_characters // TODO add fancy walls with https://en.wikipedia.org/wiki/Box-drawing_characters
self.set_cell(buf, x, y, "#", FG_BROWN, Color::Gray); self.set_cell(buf, x, y, "#", FG_BROWN, FG_BROWN);
} }
StructureElement::Floor => { StructureElement::Floor => {
self.set_cell(buf, x, y, " ", FG_BROWN, Color::Gray); self.set_cell(buf, x, y, " ", FG_BROWN, BACKGROUND);
} }
StructureElement::StairDown => { StructureElement::StairDown => {
self.set_cell(buf, x, y, ">", Color::Black, Color::Gray); self.set_cell(buf, x, y, ">", Color::White, BACKGROUND);
} }
StructureElement::StairUp => { StructureElement::StairUp => {
self.set_cell(buf, x, y, "<", Color::Black, Color::Gray); self.set_cell(buf, x, y, "<", Color::White, BACKGROUND);
} }
StructureElement::Unknown => { StructureElement::Unknown => {
self.set_cell(buf, x, y, "", Color::DarkGray, Color::Gray); self.set_cell(buf, x, y, "", Color::Gray, BACKGROUND);
} }
} }
} }
(_, Some(m), _) => { (_, Some(m), _) => {
let (s, c) = m.get_representation(); let (s, c) = m.get_representation();
self.set_cell(buf, x, y, s, c, Color::Gray); self.set_cell(buf, x, y, s, c, BACKGROUND);
} }
(_, _, Some(t)) => { (_, _, Some(t)) => {
let (s, c) = t.get_representation(); let (s, c) = t.get_representation();
self.set_cell(buf, x, y, s, c, Color::Gray); self.set_bold_cell(buf, x, y, s, c, BACKGROUND);
} }
(None, None, None) => {} (None, None, None) => {}
}; };
@@ -80,7 +88,7 @@ impl StatefulWidget for LevelWidget {
let player_pos = player.get_immutable_position(); let player_pos = player.get_immutable_position();
let player_x = al + player_pos.get_x() as u16; let player_x = al + player_pos.get_x() as u16;
let player_y = at + player_pos.get_y() as u16; let player_y = at + player_pos.get_y() as u16;
self.set_cell(buf, player_x, player_y, "8", Color::Red, Color::Gray); self.set_cell(buf, player_x, player_y, "8", Color::LightRed, BACKGROUND);
} }
} }
} }

View File

@@ -14,7 +14,6 @@ use crossterm::{
ExecutableCommand, ExecutableCommand,
}; };
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::block::{Position, Title};
use ratatui::widgets::{Block, BorderType, Borders, Wrap}; use ratatui::widgets::{Block, BorderType, Borders, Wrap};
use ratatui::{ use ratatui::{
prelude::{CrosstermBackend, Terminal}, prelude::{CrosstermBackend, Terminal},
@@ -32,10 +31,15 @@ mod constants;
mod game; mod game;
mod level; mod level;
mod level_generator; mod level_generator;
mod level_ladder;
mod level_widget; mod level_widget;
mod monster; mod monsters;
mod player; mod player;
mod position; mod position;
mod room;
const BACKGROUND_COLOR: Color = Color::DarkGray;
const PANEL_BACKGROUND_COLOR: Color = Color::Black;
// //
fn main() -> Result<()> { fn main() -> Result<()> {
@@ -52,7 +56,7 @@ fn main() -> Result<()> {
terminal.draw(|frame| { terminal.draw(|frame| {
let mut area = frame.area(); let mut area = frame.area();
frame.render_widget( frame.render_widget(
Block::default().style(Style::default().bg(Color::Green)), Block::default().style(Style::default().bg(BACKGROUND_COLOR)),
area, area,
); );
@@ -99,20 +103,19 @@ fn main() -> Result<()> {
height: map_area.height / 2 + 1, height: map_area.height / 2 + 1,
}; };
let block = Block::default() let block = Block::default()
.title( .title_top(
Title::from(format!(" {} ", game.get_player().get_name())) Line::from(format!(" {} ", game.get_player().get_name())).centered()
.alignment(Alignment::Center)
.position(Position::Top),
) )
.borders(Borders::TOP) .borders(Borders::TOP)
.border_style(Style::default().fg(Color::White)) .border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Blue)); .style(Style::default().bg(PANEL_BACKGROUND_COLOR));
frame.render_widget( frame.render_widget(
Paragraph::new(format!( Paragraph::new(format!(
"Health: {} of {}\nExp: {}\nGold: {}\nLevel: {} of {}\nInventory used: {} of {}", "Health: {} of {}\nDefense: {}\nExp: {}\nGold: {}\nLevel: {} of {}\nInventory used: {} of {}",
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().defense(),
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() + 1, game.get_player().get_immutable_position().get_level() + 1,
@@ -132,15 +135,13 @@ fn main() -> Result<()> {
}; };
// Display the latest messages from the game to the user // Display the latest messages from the game to the user
let block = Block::default() let block = Block::default()
.title( .title_top(
Title::from(" messages ") Line::from(" messages ").centered()
.alignment(Alignment::Center)
.position(Position::Top),
) )
.borders(Borders::TOP) .borders(Borders::TOP)
.border_style(Style::default().fg(Color::White)) .border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Blue)); .style(Style::default().bg(PANEL_BACKGROUND_COLOR));
let paragraph1 = if game.messages.is_empty() { let paragraph1 = if game.messages.is_empty() {
"".to_string() "".to_string()
@@ -217,12 +218,8 @@ fn main() -> Result<()> {
area.width = 40; area.width = 40;
area.height = 20; area.height = 20;
let block = Block::default() let block = Block::default()
.title( .title_top(Line::from(" Game ended ").centered())
Title::from(" Game ended ") .title_bottom(Line::from("Press `q` to quit!"))
.alignment(Alignment::Center)
.position(Position::Top),
)
.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))
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)

View File

@@ -16,6 +16,7 @@ pub trait Monster: HasPosition {
#[cfg(test)] #[cfg(test)]
fn get_life(&self) -> usize; fn get_life(&self) -> usize;
fn damage(&self) -> usize; fn damage(&self) -> usize;
fn defense(&self) -> usize;
} }
#[derive(CreateMonsters, PartialEq, Eq, Hash, Clone, Copy)] #[derive(CreateMonsters, PartialEq, Eq, Hash, Clone, Copy)]
@@ -45,10 +46,11 @@ create_monster!(
name:"rat".to_string(), name:"rat".to_string(),
life: 2, life: 2,
symbol: String::from("r"), symbol: String::from("r"),
color: Color::Black, color: Color::White,
experience_gain: 5, experience_gain: 5,
ticks_between_steps: 5, ticks_between_steps: 5,
damage_range: 1..=2, damage_range: 1..=2,
defense: 0,
); );
create_monster!( create_monster!(
@@ -56,10 +58,11 @@ create_monster!(
name:"spider".to_string(), name:"spider".to_string(),
life: 3, life: 3,
symbol: String::from("s"), symbol: String::from("s"),
color: Color::Blue, color: Color::Yellow,
experience_gain: 7, experience_gain: 7,
ticks_between_steps: 7, ticks_between_steps: 7,
damage_range: 2..=3, damage_range: 2..=3,
defense: 0,
); );
create_monster!( create_monster!(
@@ -67,10 +70,11 @@ create_monster!(
name: "orc".to_string(), name: "orc".to_string(),
life: 4, life: 4,
symbol: String::from("O"), symbol: String::from("O"),
color: Color::DarkGray, color: Color::Gray,
experience_gain: 10, experience_gain: 10,
ticks_between_steps: 10, ticks_between_steps: 10,
damage_range: 2..=3, damage_range: 2..=3,
defense: 0,
); );
create_monster!( create_monster!(
@@ -78,10 +82,11 @@ create_monster!(
name: "snake".to_string(), name: "snake".to_string(),
life: 3, life: 3,
symbol: String::from("s"), symbol: String::from("s"),
color: Color::Black, color: Color::White,
experience_gain: 10, experience_gain: 10,
ticks_between_steps: 20, ticks_between_steps: 20,
damage_range: 1..=4, damage_range: 1..=4,
defense: 0,
); );
create_monster!( create_monster!(
@@ -89,10 +94,11 @@ create_monster!(
name: "skeleton".to_string(), name: "skeleton".to_string(),
life: 3, life: 3,
symbol: String::from("S"), symbol: String::from("S"),
color: Color::DarkGray, color: Color::Gray,
experience_gain: 20, experience_gain: 20,
ticks_between_steps: 10, ticks_between_steps: 10,
damage_range: 1..=5, damage_range: 1..=5,
defense: 0,
); );
pub fn create_monster_by_type(monster_type: &MonsterTypes, position: Position) -> Box<dyn Monster> { pub fn create_monster_by_type(monster_type: &MonsterTypes, position: Position) -> Box<dyn Monster> {

View File

@@ -2,6 +2,7 @@ use rand::Rng;
use std::cmp::{max, min}; use std::cmp::{max, min};
use crate::artifacts::Potion; use crate::artifacts::Potion;
use crate::level_ladder::get_level_ladder;
use crate::position::{HasPosition, Position}; use crate::position::{HasPosition, Position};
pub struct Player { pub struct Player {
@@ -13,6 +14,8 @@ pub struct Player {
experience: usize, experience: usize,
inventory: Vec<Potion>, inventory: Vec<Potion>,
inventory_slots: usize, inventory_slots: usize,
level: usize,
defense: usize,
} }
impl Player { impl Player {
@@ -26,6 +29,8 @@ impl Player {
experience: 0, experience: 0,
inventory: vec![], inventory: vec![],
inventory_slots: 2, inventory_slots: 2,
level: 1,
defense: 0,
} }
} }
pub fn get_name(&self) -> String { pub fn get_name(&self) -> String {
@@ -59,8 +64,28 @@ impl Player {
self.gold self.gold
} }
pub fn gain_experience(&mut self, amount: usize) { pub fn gain_experience(&mut self, amount: usize) -> bool {
self.experience += amount let mut result = false;
for (i, level_step) in get_level_ladder().enumerate() {
if self.experience <= level_step && self.experience + amount > level_step {
self.level += 1;
// make life gain depend on level?
self.max_life += 5;
self.defense += 1;
self.life = self.max_life;
result = true;
break;
}
if i > self.level {
break;
}
}
self.experience += amount;
result
}
pub fn get_experience_level(&self) -> usize {
self.level
} }
pub fn get_experience(&self) -> usize { pub fn get_experience(&self) -> usize {
@@ -71,6 +96,10 @@ impl Player {
rand::thread_rng().gen_range(1..4) rand::thread_rng().gen_range(1..4)
} }
pub fn defense(&self) -> usize {
self.defense
}
pub fn add_to_inventory(&mut self, potion: &Potion) -> bool { pub fn add_to_inventory(&mut self, potion: &Potion) -> bool {
if self.inventory.len() < self.inventory_slots { if self.inventory.len() < self.inventory_slots {
self.inventory.push(*potion); self.inventory.push(*potion);
@@ -116,6 +145,8 @@ fn test_get_name() {
experience: 0, experience: 0,
inventory: vec![], inventory: vec![],
inventory_slots: 1, inventory_slots: 1,
level: 0,
defense: 0,
}; };
assert_eq!(p.get_name(), "Teddy Tester"); assert_eq!(p.get_name(), "Teddy Tester");
} }
@@ -141,6 +172,8 @@ fn test_change_life() {
experience: 0, experience: 0,
inventory: vec![], inventory: vec![],
inventory_slots: 1, inventory_slots: 1,
level: 0,
defense: 0,
}; };
assert_eq!(p.get_life(), 5); assert_eq!(p.get_life(), 5);
p.change_life(-2); p.change_life(-2);
@@ -176,6 +209,8 @@ fn test_max_life() {
experience: 0, experience: 0,
inventory: vec![], inventory: vec![],
inventory_slots: 1, inventory_slots: 1,
level: 0,
defense: 0,
}; };
assert_eq!(p.get_max_life(), 10); assert_eq!(p.get_max_life(), 10);
} }

280
src/room.rs Normal file
View File

@@ -0,0 +1,280 @@
use std::{
cmp::{max, min},
ops::RangeInclusive,
};
use crate::{
constants::{
LEVEL_HEIGHT, LEVEL_WIDTH, ROOM_HEIGHT, ROOM_MIN_HEIGHT, ROOM_MIN_WIDTH, ROOM_WIDTH,
},
level::StructureElement,
};
use rand::rngs::ThreadRng;
use rand::Rng;
#[derive(PartialEq, Copy, Clone, Eq, Hash, Debug)]
pub enum RoomType {
Start,
End,
StairUp,
StairDown,
BasicRoom,
ArtifactRoom,
MonsterRoom,
EmptyRoom,
}
#[derive(Copy, Clone, Debug)]
pub struct Connection {
pub start_pos: (usize, usize),
pub end_pos: (usize, usize),
}
impl Connection {
pub fn render(
&self,
rng: &mut ThreadRng,
tgt: &mut [[StructureElement; LEVEL_HEIGHT]; LEVEL_WIDTH],
) {
fn calc_range(a: usize, b: usize) -> RangeInclusive<usize> {
if a > b {
b..=a
} else {
a..=b
}
}
// the tuples are (col, row)
let abs_d_col =
max(self.start_pos.0, self.end_pos.0) - min(self.start_pos.0, self.end_pos.0);
let abs_d_row =
max(self.start_pos.1, self.end_pos.1) - min(self.start_pos.1, self.end_pos.1);
if abs_d_col == 0 {
for row in calc_range(self.end_pos.1, self.start_pos.1) {
tgt[self.end_pos.0][row] = StructureElement::Floor;
}
} else if abs_d_row == 0 {
for col in calc_range(self.end_pos.0, self.start_pos.0) {
tgt[col][self.end_pos.1] = StructureElement::Floor;
}
} else {
let d_row = self.end_pos.1 as i32 - self.start_pos.1 as i32;
if d_row > 0 {
// more up/down
let d_row = self.end_pos.1 - self.start_pos.1;
let vert_offset = if d_row > 3 {
rng.gen_range(1..=d_row - 1)
} else {
d_row / 2
};
for r in self.start_pos.1..=(self.start_pos.1 + vert_offset) {
tgt[self.start_pos.0][r] = StructureElement::Floor;
}
for c in calc_range(self.end_pos.0, self.start_pos.0) {
tgt[c][self.start_pos.1 + vert_offset] = StructureElement::Floor;
}
for r in (self.start_pos.1 + vert_offset)..=self.end_pos.1 {
tgt[self.end_pos.0][r] = StructureElement::Floor;
}
} else {
// more left/right
let d_col = self.end_pos.0 - self.start_pos.0;
let horizont_offset = if d_col > 3 {
rng.gen_range(1..=d_col - 1)
} else {
d_col / 2
};
for tgt_col in tgt
.iter_mut()
.skip(self.start_pos.0)
.take(horizont_offset + 1)
{
tgt_col[self.start_pos.1] = StructureElement::Floor;
}
for r in calc_range(self.end_pos.1, self.start_pos.1) {
tgt[self.start_pos.0 + horizont_offset][r] = StructureElement::Floor;
}
for tgt_col in tgt
.iter_mut()
.take(self.end_pos.0 + 1)
.skip(self.start_pos.0 + horizont_offset)
{
tgt_col[self.end_pos.1] = StructureElement::Floor;
}
}
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct Room {
pub kind: RoomType,
pub offset_x: usize,
pub offset_y: usize,
pub width: usize,
pub height: usize,
pub special: (usize, usize),
pub connection_down: Option<Connection>,
pub connection_right: Option<Connection>,
}
impl Room {
pub fn new(rng: &mut ThreadRng) -> Self {
let width = rng.gen_range(ROOM_MIN_WIDTH..ROOM_WIDTH);
let height = rng.gen_range(ROOM_MIN_HEIGHT..ROOM_HEIGHT);
let offset_x = rng.gen_range(0..(ROOM_WIDTH - width));
let offset_y = rng.gen_range(0..(ROOM_HEIGHT - height));
let sx = offset_x + rng.gen_range(1..width - 1);
let sy = offset_y + rng.gen_range(1..height - 1);
Self {
kind: RoomType::EmptyRoom,
offset_x,
offset_y,
width,
height,
special: (sx, sy),
connection_down: None,
connection_right: None,
}
}
// pub fn get_x_range(&self) -> Range<usize> {
// self.offset_x..self.offset_x + self.width
// }
// pub fn get_y_range(&self) -> Range<usize> {
// self.offset_y..self.offset_y + self.height
// }
pub fn render(
&self,
tgt: &mut [[StructureElement; LEVEL_HEIGHT]; LEVEL_WIDTH],
col: usize,
row: usize,
) -> (usize, usize) {
let top = 1 + row * ROOM_HEIGHT;
let left = 1 + col * ROOM_WIDTH;
for col in tgt.iter_mut().skip(left).take(ROOM_WIDTH) {
//left..left + ROOM_WIDTH {
for row_element in col.iter_mut().skip(top).take(ROOM_HEIGHT) {
// top..top + ROOM_HEIGHT {
*row_element = StructureElement::Wall;
}
}
if self.kind == RoomType::EmptyRoom {
return (0, 0);
}
// render floor elements
for x in 0..self.width {
for y in 0..self.height {
tgt[left + self.offset_x + x][top + self.offset_y + y] = StructureElement::Floor;
}
}
match self.kind {
RoomType::Start => {
tgt[left + self.special.0][top + self.special.1] = StructureElement::Start;
}
RoomType::End => {
tgt[left + self.special.0][top + self.special.1] = StructureElement::End;
}
RoomType::StairUp => {
tgt[left + self.special.0][top + self.special.1] = StructureElement::StairUp;
}
RoomType::StairDown => {
tgt[left + self.special.0][top + self.special.1] = StructureElement::StairDown;
}
_ => {}
};
(left + self.special.0, top + self.special.1)
}
}
#[test]
fn test_room_creation() {
let mut rng = rand::thread_rng();
let room = Room::new(&mut rng);
assert_eq!(room.kind, RoomType::EmptyRoom);
for _ in 0..1000 {
let room = Room::new(&mut rng);
assert!((3..=ROOM_WIDTH).contains(&room.width));
}
}
#[cfg(test)]
fn all_wall(
tgt: &mut [[StructureElement; LEVEL_HEIGHT]; LEVEL_WIDTH],
col: usize,
row: usize,
) -> bool {
let top = 1 + row * ROOM_HEIGHT;
let left = 1 + col * ROOM_WIDTH;
for x in left..left + ROOM_WIDTH {
for y in top..top + ROOM_HEIGHT {
if tgt[x][y] != StructureElement::Wall {
return false;
}
}
}
true
}
#[cfg(test)]
fn has_structure_element(
tgt: &mut [[StructureElement; LEVEL_HEIGHT]; LEVEL_WIDTH],
col: usize,
row: usize,
element: StructureElement,
) -> bool {
let top = 1 + row * ROOM_HEIGHT;
let left = 1 + col * ROOM_WIDTH;
for x in left..left + ROOM_WIDTH {
for y in top..top + ROOM_HEIGHT {
if tgt[x][y] == element {
return true;
}
}
}
false
}
#[test]
fn test_room_render_empty() {
let mut rng = rand::thread_rng();
let room = Room::new(&mut rng);
assert_eq!(room.kind, RoomType::EmptyRoom);
let mut structure = [[StructureElement::Floor; LEVEL_HEIGHT]; LEVEL_WIDTH];
room.render(&mut structure, 0, 0);
assert!(all_wall(&mut structure, 0, 0));
}
#[test]
fn test_room_render_basic() {
let mut rng = rand::thread_rng();
let mut room = Room::new(&mut rng);
room.kind = RoomType::BasicRoom;
let mut structure = [[StructureElement::Floor; LEVEL_HEIGHT]; LEVEL_WIDTH];
room.render(&mut structure, 1, 3);
assert!(!all_wall(&mut structure, 1, 3));
}
#[test]
fn test_room_render_start() {
let mut rng = rand::thread_rng();
let mut room = Room::new(&mut rng);
room.kind = RoomType::Start;
let mut structure = [[StructureElement::Floor; LEVEL_HEIGHT]; LEVEL_WIDTH];
room.render(&mut structure, 1, 3);
assert!(!all_wall(&mut structure, 1, 3));
assert!(has_structure_element(
&mut structure,
1,
3,
StructureElement::Start
));
}