Initial Dungeon Crawler Game

This commit is contained in:
Daniel Lynn 2021-07-05 23:05:49 -05:00
commit 752414d8b5
10 changed files with 2304 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1954
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "dungeoncrawl"
version = "0.1.0"
authors = [ "Daniel Lynn <daniel.eric.lynn@gmail.com>" ]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bracket-lib = "~0.8.1"
legion = "=0.3.1"

43
DESIGN.md Normal file
View File

@ -0,0 +1,43 @@
# Short Desgin Document
## Project Name
Cooking with Roguelike
## Short Description
A dungeon crawler with procedurally generated levels, monsters of increasing
difficulty, and turn-based movement.
## Story
A marshmallow leaves the Pantry in search of a paradise far beyond House.
## Basic Game Loops
1. Enter dungeon level.
2. Explore, revealing the map.
3. Encounter enemies whom the player fights or flees.
4. Find power-ups and use them to strengthen the player.
5. Locate the exit to the level - go to 1.
## Minimum Viable Product
1. Create a basic dungeon map.
2. Place the player and let them walk around.
3. Spawn monsters, draw them, and let the player kill them by walking into them.
4. Add health and a combat system that uses it.
5. Add healing potions.
6. Display a "game over" screen when the player dies.
7. Add the exit to the level and let the player win by reaching it.
## Stretch Goals
1. Add Fields-of-View.
2. Add more interesting dungeon designs.
3. Add some dungeon themes.
4. Add multiple layers to the dungeon, with Paradise exit on the last one.
5. Add varied weapons to the game.
6. Move to a data-driven design for spawning enemies.
7. Consider some visual effects to make the combat more visceral.
8. Consider keeping score.

BIN
resources/dungeonfont.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

26
src/camera.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::prelude::*;
pub struct Camera {
pub left_x: i32,
pub right_x: i32,
pub top_y: i32,
pub bottom_y: i32,
}
impl Camera {
pub fn new(player_position: Point) -> Self {
Self {
left_x: player_position.x - DISPLAY_HEIGHT / 2,
right_x: player_position.x + DISPLAY_HEIGHT / 2,
top_y: player_position.y - DISPLAY_HEIGHT / 2,
bottom_y: player_position.y + DISPLAY_HEIGHT / 2,
}
}
pub fn on_player_move(&mut self, player_position: Point) {
self.left_x = player_position.x - DISPLAY_WIDTH / 2;
self.right_x = player_position.x + DISPLAY_WIDTH / 2;
self.top_y = player_position.y - DISPLAY_HEIGHT / 2;
self.bottom_y = player_position.y + DISPLAY_HEIGHT / 2;
}
}

64
src/main.rs Normal file
View File

@ -0,0 +1,64 @@
mod camera;
mod map;
mod map_builder;
mod player;
mod prelude {
pub use bracket_lib::prelude::*;
pub const SCREEN_WIDTH: i32 = 80;
pub const SCREEN_HEIGHT: i32 = 50;
pub const DISPLAY_WIDTH: i32 = SCREEN_WIDTH / 2;
pub const DISPLAY_HEIGHT: i32 = SCREEN_HEIGHT / 2;
pub use crate::camera::*;
pub use crate::map::*;
pub use crate::map_builder::*;
pub use crate::player::*;
}
use prelude::*;
struct State {
map: Map,
player: Player,
camera: Camera,
}
impl State {
fn new() -> Self {
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
Self {
map: map_builder.map,
player: Player::new(map_builder.player_start),
camera: Camera::new(map_builder.player_start),
}
}
}
impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(0);
ctx.cls();
ctx.set_active_console(1);
ctx.cls();
self.player.update(ctx, &self.map, &mut self.camera);
self.map.render(ctx, &self.camera);
self.player.render(ctx, &self.camera);
}
}
fn main() -> BError {
let context = BTermBuilder::new()
.with_title("Dungeon Crawler")
.with_fps_cap(30.0)
.with_dimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT)
.with_tile_dimensions(32, 32)
.with_resource_path("resources/")
.with_font("dungeonfont.png", 32, 32)
.with_simple_console(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.with_simple_console_no_bg(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.build()?;
main_loop(context, State::new())
}

72
src/map.rs Normal file
View File

@ -0,0 +1,72 @@
use crate::prelude::*;
const NUM_TILES: usize = (SCREEN_WIDTH * SCREEN_HEIGHT) as usize;
#[derive(Clone, Copy, PartialEq)]
pub enum TileType {
Wall,
Floor,
}
pub struct Map {
pub tiles: Vec<TileType>,
}
pub fn map_idx(x: i32, y: i32) -> usize {
((y * SCREEN_WIDTH) + x) as usize
}
impl Map {
pub fn new() -> Self {
Self {
tiles: vec![TileType::Floor; NUM_TILES],
}
}
pub fn render(&self, ctx: &mut BTerm, camera: &Camera) {
ctx.set_active_console(0);
for y in camera.top_y..camera.bottom_y {
for x in camera.left_x..camera.right_x {
if self.in_bounds(Point::new(x, y)) {
let idx = map_idx(x, y);
match self.tiles[idx] {
TileType::Floor => {
ctx.set(
x - camera.left_x,
y - camera.top_y,
WHITE,
BLACK,
to_cp437('.'),
);
}
TileType::Wall => {
ctx.set(
x - camera.left_x,
y - camera.top_y,
WHITE,
BLACK,
to_cp437('#'),
);
}
}
}
}
}
}
pub fn in_bounds(&self, point: Point) -> bool {
point.x >= 0 && point.x < SCREEN_WIDTH && point.y >= 0 && point.y < SCREEN_HEIGHT
}
pub fn can_enter_tile(&self, point: Point) -> bool {
self.in_bounds(point) && self.tiles[map_idx(point.x, point.y)] == TileType::Floor
}
pub fn try_idx(&self, point: Point) -> Option<usize> {
if !self.in_bounds(point) {
None
} else {
Some(map_idx(point.x, point.y))
}
}
}

93
src/map_builder.rs Normal file
View File

@ -0,0 +1,93 @@
use crate::prelude::*;
const NUM_ROOMS: usize = 20;
pub struct MapBuilder {
pub map: Map,
pub rooms: Vec<Rect>,
pub player_start: Point,
}
impl MapBuilder {
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
player_start: Point::zero(),
};
mb.fill(TileType::Wall);
mb.build_random_rooms(rng);
mb.build_corridors(rng);
mb.player_start = mb.rooms[0].center();
mb
}
fn fill(&mut self, tile: TileType) {
self.map.tiles.iter_mut().for_each(|t| *t = tile);
}
fn build_random_rooms(&mut self, rng: &mut RandomNumberGenerator) {
while self.rooms.len() < NUM_ROOMS {
let room = Rect::with_size(
rng.range(1, SCREEN_HEIGHT - 10),
rng.range(1, SCREEN_HEIGHT - 10),
rng.range(2, 10),
rng.range(2, 10),
);
let mut overlap = false;
for r in self.rooms.iter() {
if r.intersect(&room) {
overlap = true;
}
}
if !overlap {
room.for_each(|p| {
if p.x > 0 && p.x < SCREEN_WIDTH && p.y > 0 && p.y < SCREEN_HEIGHT {
let idx = map_idx(p.x, p.y);
self.map.tiles[idx] = TileType::Floor;
}
});
self.rooms.push(room);
}
}
}
fn apply_vertical_tunnel(&mut self, y1: i32, y2: i32, x: i32) {
use std::cmp::{max, min};
for y in min(y1, y2)..=max(y1, y2) {
if let Some(idx) = self.map.try_idx(Point::new(x, y)) {
self.map.tiles[idx as usize] = TileType::Floor;
}
}
}
fn apply_horizontal_tunnel(&mut self, x1: i32, x2: i32, y: i32) {
use std::cmp::{max, min};
for x in min(x1, x2)..=max(x1, x2) {
if let Some(idx) = self.map.try_idx(Point::new(x, y)) {
self.map.tiles[idx as usize] = TileType::Floor;
}
}
}
fn build_corridors(&mut self, rng: &mut RandomNumberGenerator) {
let mut rooms = self.rooms.clone();
rooms.sort_by(|a, b| a.center().x.cmp(&b.center().x));
for (i, room) in rooms.iter().enumerate().skip(1) {
let prev = rooms[i - 1].center();
let new = room.center();
if rng.range(0, 2) == 1 {
self.apply_horizontal_tunnel(prev.x, new.x, prev.y);
self.apply_vertical_tunnel(prev.y, new.y, new.x);
} else {
self.apply_vertical_tunnel(prev.y, new.y, prev.x);
self.apply_horizontal_tunnel(prev.x, new.x, new.y);
}
}
}
}

40
src/player.rs Normal file
View File

@ -0,0 +1,40 @@
use crate::prelude::*;
pub struct Player {
pub position: Point,
}
impl Player {
pub fn new(position: Point) -> Self {
Self { position }
}
pub fn render(&self, ctx: &mut BTerm, camera: &Camera) {
ctx.set_active_console(1);
ctx.set(
self.position.x - camera.left_x,
self.position.y - camera.top_y,
WHITE,
BLACK,
to_cp437('@'),
);
}
pub fn update(&mut self, ctx: &mut BTerm, map: &Map, camera: &mut Camera) {
if let Some(key) = ctx.key {
let delta = match key {
VirtualKeyCode::Left | VirtualKeyCode::A => Point::new(-1, 0),
VirtualKeyCode::Right | VirtualKeyCode::D => Point::new(1, 0),
VirtualKeyCode::Up | VirtualKeyCode::W => Point::new(0, -1),
VirtualKeyCode::Down | VirtualKeyCode::S => Point::new(0, 1),
_ => Point::zero(),
};
let new_position = self.position + delta;
if map.can_enter_tile(new_position) {
self.position = new_position;
camera.on_player_move(new_position);
}
}
}
}