From fc0f8da4b51d737158d4c8208acffd390108478a Mon Sep 17 00:00:00 2001 From: Olle Wreede Date: Sun, 31 Jul 2022 15:34:26 +0200 Subject: [PATCH 1/3] Add Rusty Roguelike for Macroquad --- rusty_roguelike-macroquad/.gitignore | 1 + rusty_roguelike-macroquad/Cargo.toml | 28 ++ rusty_roguelike-macroquad/README.md | 17 ++ .../assets/dungeonfont.png | Bin 0 -> 40853 bytes rusty_roguelike-macroquad/assets/template.ron | 75 +++++ rusty_roguelike-macroquad/src/camera_view.rs | 26 ++ rusty_roguelike-macroquad/src/components.rs | 97 +++++++ .../src/macroquad_utils.rs | 156 +++++++++++ rusty_roguelike-macroquad/src/main.rs | 262 ++++++++++++++++++ rusty_roguelike-macroquad/src/map.rs | 100 +++++++ .../src/map_builder/automata.rs | 88 ++++++ .../src/map_builder/drunkard.rs | 87 ++++++ .../src/map_builder/empty.rs | 27 ++ .../src/map_builder/mod.rs | 173 ++++++++++++ .../src/map_builder/prefab.rs | 82 ++++++ .../src/map_builder/rooms.rs | 28 ++ .../src/map_builder/themes.rs | 37 +++ rusty_roguelike-macroquad/src/spawner/mod.rs | 39 +++ .../src/spawner/template.rs | 108 ++++++++ .../src/systems/chasing.rs | 65 +++++ .../src/systems/combat.rs | 60 ++++ .../src/systems/end_turn.rs | 37 +++ .../src/systems/entity_render.rs | 25 ++ rusty_roguelike-macroquad/src/systems/fov.rs | 15 + rusty_roguelike-macroquad/src/systems/hud.rs | 50 ++++ .../src/systems/map_render.rs | 35 +++ rusty_roguelike-macroquad/src/systems/mod.rs | 63 +++++ .../src/systems/movement.rs | 31 +++ .../src/systems/player_input.rs | 128 +++++++++ .../src/systems/random_move.rs | 51 ++++ .../src/systems/tooltips.rs | 29 ++ .../src/systems/use_items.rs | 36 +++ rusty_roguelike-macroquad/src/turn_state.rs | 9 + 33 files changed, 2065 insertions(+) create mode 100644 rusty_roguelike-macroquad/.gitignore create mode 100644 rusty_roguelike-macroquad/Cargo.toml create mode 100644 rusty_roguelike-macroquad/README.md create mode 100644 rusty_roguelike-macroquad/assets/dungeonfont.png create mode 100644 rusty_roguelike-macroquad/assets/template.ron create mode 100644 rusty_roguelike-macroquad/src/camera_view.rs create mode 100644 rusty_roguelike-macroquad/src/components.rs create mode 100644 rusty_roguelike-macroquad/src/macroquad_utils.rs create mode 100644 rusty_roguelike-macroquad/src/main.rs create mode 100644 rusty_roguelike-macroquad/src/map.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/automata.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/drunkard.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/empty.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/mod.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/prefab.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/rooms.rs create mode 100644 rusty_roguelike-macroquad/src/map_builder/themes.rs create mode 100644 rusty_roguelike-macroquad/src/spawner/mod.rs create mode 100644 rusty_roguelike-macroquad/src/spawner/template.rs create mode 100644 rusty_roguelike-macroquad/src/systems/chasing.rs create mode 100644 rusty_roguelike-macroquad/src/systems/combat.rs create mode 100644 rusty_roguelike-macroquad/src/systems/end_turn.rs create mode 100644 rusty_roguelike-macroquad/src/systems/entity_render.rs create mode 100644 rusty_roguelike-macroquad/src/systems/fov.rs create mode 100644 rusty_roguelike-macroquad/src/systems/hud.rs create mode 100644 rusty_roguelike-macroquad/src/systems/map_render.rs create mode 100644 rusty_roguelike-macroquad/src/systems/mod.rs create mode 100644 rusty_roguelike-macroquad/src/systems/movement.rs create mode 100644 rusty_roguelike-macroquad/src/systems/player_input.rs create mode 100644 rusty_roguelike-macroquad/src/systems/random_move.rs create mode 100644 rusty_roguelike-macroquad/src/systems/tooltips.rs create mode 100644 rusty_roguelike-macroquad/src/systems/use_items.rs create mode 100644 rusty_roguelike-macroquad/src/turn_state.rs diff --git a/rusty_roguelike-macroquad/.gitignore b/rusty_roguelike-macroquad/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/rusty_roguelike-macroquad/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rusty_roguelike-macroquad/Cargo.toml b/rusty_roguelike-macroquad/Cargo.toml new file mode 100644 index 000000000..9d2d9dab0 --- /dev/null +++ b/rusty_roguelike-macroquad/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rusty-dungeon" +version = "0.5.0" +authors = ["Olle Wreede "] +edition = "2021" + +[dependencies] +macroquad = "0.3.3" +legion = { version = "=0.3.1", default-features = false, features = ["codegen"] } +getrandom = { version = "0.2", features = ["js"] } +bracket-pathfinding = "0.8.4" +nanoserde = "0.1.26" +lazy_static = "1.4.0" + +# Doesn't work with android build +[profile.dev.package.'*'] +opt-level = 3 + +[profile.release] +opt-level = 'z' +lto = true +panic = 'abort' +codegen-units = 1 + +[package.metadata.scripts] +build-web = "bin/build-web" +serve-web = "bin/serve-web" + diff --git a/rusty_roguelike-macroquad/README.md b/rusty_roguelike-macroquad/README.md new file mode 100644 index 000000000..b6765f138 --- /dev/null +++ b/rusty_roguelike-macroquad/README.md @@ -0,0 +1,17 @@ +# Rusty Roguelike for Macroquead + +A small roguelike based on the example in the Hands-on Rust book by Herbert +Wolverson, but written in Macroquad. + +Buy the book from Pragmatic Programmer: + +* https://pragprog.com/titles/hwrust/hands-on-rust/ + +## Acknowledgements + + * Herbert Wolverson for the base code + * Item icons by Caz Wolf https://cazwolf.itch.io/ + * Floor tiles made by homyakokryak + * Assorted CC0/public domain images from opengameart.org + * Knight by PolygonDan + * Potions by Admurin: https://admurin.itch.io/ diff --git a/rusty_roguelike-macroquad/assets/dungeonfont.png b/rusty_roguelike-macroquad/assets/dungeonfont.png new file mode 100644 index 0000000000000000000000000000000000000000..86664ba50be365143eb39b1a52217342f85c49cf GIT binary patch literal 40853 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 UH_Zcf(*f{>!&+i04Beyt2U$4RC;$Ke literal 0 HcmV?d00001 diff --git a/rusty_roguelike-macroquad/assets/template.ron b/rusty_roguelike-macroquad/assets/template.ron new file mode 100644 index 000000000..a62525245 --- /dev/null +++ b/rusty_roguelike-macroquad/assets/template.ron @@ -0,0 +1,75 @@ +( + entities : [ + ( + entity_type: Item, + name : "Healing Potion", sprite : 33, levels : [ 0, 1, 2 ], + provides: [ ("Healing", 6) ], + frequency: 2 + ), + ( + entity_type: Item, + name : "Weak Healing Potion", sprite : 33, levels : [ 0, 1, 2 ], + provides: [ ("Healing", 2) ], + frequency: 2 + ), + ( + entity_type: Item, + name : "Dungeon Map", sprite : 123, levels : [ 0, 1, 2 ], + provides: [ ("MagicMap", 0) ], + frequency: 1 + ), + ( + entity_type: Item, + name : "Short Sword", sprite: 115, levels: [ 0, 1, 2 ], + frequency: 1, + base_damage: 1 + ), + ( + entity_type: Item, + name : "2-handed Sword", sprite: 83, levels: [ 0, 1, 2 ], + frequency: 1, + base_damage: 2 + ), + ( + entity_type: Item, + name : "Claymore", sprite: 47, levels: [ 2 ], + frequency: 1, + base_damage: 3 + ), + ( + entity_type: Enemy, + name : "Bat", sprite : 98, levels : [ 0 ], + hp : 1, + frequency: 8, + base_damage: 1 + ), + ( + entity_type: Enemy, + name : "Kobold", sprite : 103, levels : [ 0, 1 ], + hp : 2, + frequency: 5, + base_damage: 1 + ), + ( + entity_type: Enemy, + name : "2-headed Ogre", sprite : 111, levels : [ 0, 1, 2 ], + hp : 4, + frequency: 2, + base_damage: 1 + ), + ( + entity_type: Enemy, + name : "Deep Troll", sprite : 79, levels : [ 1, 2 ], + hp : 6, + frequency: 2, + base_damage: 2 + ), + ( + entity_type: Enemy, + name : "Lindwyrm", sprite : 69, levels : [ 2 ], + hp : 8, + frequency: 1, + base_damage: 3 + ), + ], +) diff --git a/rusty_roguelike-macroquad/src/camera_view.rs b/rusty_roguelike-macroquad/src/camera_view.rs new file mode 100644 index 000000000..bf8adcfe3 --- /dev/null +++ b/rusty_roguelike-macroquad/src/camera_view.rs @@ -0,0 +1,26 @@ +use crate::prelude::*; + +pub struct CameraView { + pub left_x: i32, + pub right_x: i32, + pub top_y: i32, + pub bottom_y: i32, +} + +impl CameraView { + pub fn new(player_position: Point) -> Self { + Self { + left_x: player_position.x - DISPLAY_WIDTH / 2, + right_x: player_position.x + DISPLAY_WIDTH / 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; + } +} diff --git a/rusty_roguelike-macroquad/src/components.rs b/rusty_roguelike-macroquad/src/components.rs new file mode 100644 index 000000000..596847a5a --- /dev/null +++ b/rusty_roguelike-macroquad/src/components.rs @@ -0,0 +1,97 @@ +use crate::prelude::*; +use std::collections::HashSet; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Render { + pub color: Color, + pub sprite: Sprite, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Player { + pub map_level: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Enemy; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct MovingRandomly; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WantsToMove { + pub entity: Entity, + pub destination: Point, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Health { + pub current: i32, + pub max: i32, +} + +#[derive(Clone, PartialEq)] +pub struct Name(pub String); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WantsToAttack { + pub attacker: Entity, + pub victim: Entity, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ChasingPlayer; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Item; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AmuletOfYala; + +#[derive(Clone, Debug, PartialEq)] +pub struct FieldOfView { + pub visible_tiles: HashSet, + pub radius: i32, + pub is_dirty: bool, +} + +impl FieldOfView { + pub fn new(radius: i32) -> Self { + Self { + visible_tiles: HashSet::new(), + radius, + is_dirty: true, + } + } + + pub fn clone_dirty(&self) -> Self { + Self { + visible_tiles: HashSet::new(), + radius: self.radius, + is_dirty: true, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ProvidesHealing { + pub amount: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ProvidesDungeonMap; + +#[derive(Clone, PartialEq)] +pub struct Carried(pub Entity); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ActivateItem { + pub used_by: Entity, + pub item: Entity, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Damage(pub i32); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Weapon; diff --git a/rusty_roguelike-macroquad/src/macroquad_utils.rs b/rusty_roguelike-macroquad/src/macroquad_utils.rs new file mode 100644 index 000000000..a5665d6d4 --- /dev/null +++ b/rusty_roguelike-macroquad/src/macroquad_utils.rs @@ -0,0 +1,156 @@ +use crate::prelude::*; +use macroquad::math::Rect; + +pub fn tile_size() -> Vec2 { + vec2(tile_width(), tile_height()) +} + +pub fn tile_width() -> f32 { + screen_width() / DISPLAY_WIDTH as f32 +} + +pub fn tile_height() -> f32 { + screen_height() / DISPLAY_HEIGHT as f32 +} + +pub fn tile_pos_x(x: i32) -> f32 { + x as f32 * tile_width() +} + +pub fn tile_pos_y(y: i32) -> f32 { + y as f32 * tile_height() +} + +pub fn text_pos_x(x: i32) -> f32 { + x as f32 * screen_width() / SCREEN_WIDTH as f32 +} + +pub fn mouse_tile_position() -> (i32, i32) { + let pos = mouse_position(); + ( + (pos.0 / tile_width()) as i32, + (pos.1 / tile_height()) as i32, + ) +} + +pub type Sprite = u16; + +#[derive(Debug)] +pub struct TileSet { + pub texture: Texture2D, + pub tile_width: i32, + pub tile_height: i32, + pub columns: u16, +} + +impl TileSet { + pub const SPRITE_PLAYER: Sprite = 64; + pub const SPRITE_WALL: Sprite = 35; + pub const SPRITE_GROUND: Sprite = 59; + pub const SPRITE_STAIRS: Sprite = 62; + pub const SPRITE_AMULET: Sprite = 124; + + pub fn sprite_rect(&self, ix: Sprite) -> Rect { + let sw = self.tile_width as f32; + let sh = self.tile_height as f32; + let sx = (ix % self.columns) as f32 * sw as f32; + let sy = (ix / self.columns) as f32 * sh as f32; + + Rect::new(sx, sy, sw, sh) + } + + pub fn draw_tile(&self, sprite: Sprite, color: Color, x: i32, y: i32) { + let spr_rect = self.sprite_rect(sprite); + draw_texture_ex( + self.texture, + tile_pos_x(x), + tile_pos_y(y), + color, + DrawTextureParams { + dest_size: Some(tile_size()), + source: Some(spr_rect), + ..Default::default() + }, + ); + } +} + +pub fn print_centered(line: i32, text: S) +where + S: ToString, +{ + print_color_centered(line, text, WHITE); +} + +pub fn print_color_centered(line: i32, text: S, text_color: Color) +where + S: ToString, +{ + let x = SCREEN_WIDTH / 2 - (text.to_string().len() / 2) as i32; + print_color_pos(Point::new(x, line), text, text_color); +} + +pub fn print_color_right(pos: Point, text: S, text_color: Color) +where + S: ToString, +{ + let offset = Point::new(text.to_string().len() as i32, 0); + print_color_pos(pos - offset, text, text_color); +} + +pub fn print_pos(pos: Point, text: S) +where + S: ToString, +{ + print_color_pos(pos, text, WHITE); +} + +pub fn print_color_pos(pos: Point, text: S, text_color: Color) +where + S: ToString, +{ + let text_params = TextParams { + color: text_color, + font_size: tile_height() as u16, + ..TextParams::default() + }; + let dimensions = measure_text( + &text.to_string(), + Some(Font::default()), + text_params.font_size, + text_params.font_scale, + ); + let x = text_pos_x(pos.x); + let fudge = (dimensions.height - dimensions.offset_y) / 2.; + let y = tile_pos_y(pos.y) + fudge + dimensions.offset_y; + draw_text_ex(&text.to_string(), x, y, text_params); +} + +pub fn bar_horizontal( + pos: Point, + width: i32, + current: i32, + max: i32, + color: Color, + background: Color, +) { + let x = tile_pos_x(pos.x); + let y = tile_pos_y(pos.y); + let bar_width = tile_pos_x(width); + let current_width = current as f32 / max as f32 * bar_width; + draw_rectangle(x, y, bar_width, tile_height(), background); + draw_rectangle(x, y, current_width, tile_height(), color); +} + +pub fn random_slice_index(slice: &[T]) -> Option { + if slice.is_empty() { + None + } else { + let size = slice.len(); + if size == 1 { + Some(0) + } else { + Some(rand::gen_range(1, size as i32) as usize - 1) + } + } +} diff --git a/rusty_roguelike-macroquad/src/main.rs b/rusty_roguelike-macroquad/src/main.rs new file mode 100644 index 000000000..02561156c --- /dev/null +++ b/rusty_roguelike-macroquad/src/main.rs @@ -0,0 +1,262 @@ +mod camera_view; +mod components; +mod macroquad_utils; +mod map; +mod map_builder; +mod spawner; +mod systems; +mod turn_state; + +#[macro_use] +extern crate lazy_static; + +lazy_static! { + pub static ref RANDOM_FLOOR_TILES: Vec = gen_random_tiles(0, 16); + pub static ref RANDOM_TREE_TILES: Vec = gen_random_tiles(16, 20); +} + +fn gen_random_tiles(lower: usize, upper: usize) -> Vec { + (0..=(SCREEN_WIDTH * SCREEN_HEIGHT)) + .map(|_| rand::gen_range(lower, upper) as Sprite) + .collect() +} + +mod prelude { + pub use bracket_pathfinding::prelude::*; + pub use legion::systems::CommandBuffer; + pub use legion::world::SubWorld; + pub use legion::*; + pub use macroquad::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_view::*; + pub use crate::components::*; + pub use crate::macroquad_utils::*; + pub use crate::map::*; + pub use crate::map_builder::*; + pub use crate::spawner::*; + pub use crate::systems::*; + pub use crate::turn_state::*; + pub use crate::RANDOM_FLOOR_TILES; + pub use crate::RANDOM_TREE_TILES; +} + +use prelude::*; + +struct State { + ecs: World, + resources: Resources, + input_systems: Schedule, + player_systems: Schedule, + monster_systems: Schedule, + texture: Texture2D, +} + +impl State { + async fn new(texture: Texture2D) -> Self { + rand::srand(miniquad::date::now() as u64); + let mut ecs = World::default(); + let mut resources = Resources::default(); + let mut map_builder = MapBuilder::new(); + let tileset = Self::tileset(texture); + spawn_player(&mut ecs, map_builder.player_start); + let exit_idx = map_builder.map.point2d_to_index(map_builder.amulet_start); + map_builder.map.tiles[exit_idx] = TileType::Exit; + spawn_level(&mut ecs, 0, &map_builder.monster_spawns).await; + resources.insert(map_builder.map); + resources.insert(CameraView::new(map_builder.player_start)); + resources.insert(tileset); + resources.insert(TurnState::AwaitingInput); + resources.insert(map_builder.theme); + Self { + ecs, + resources, + input_systems: build_input_scheduler(), + player_systems: build_player_scheduler(), + monster_systems: build_monster_scheduler(), + texture, + } + } + + fn tileset(texture: Texture2D) -> TileSet { + TileSet { + texture: texture, + tile_width: 32, + tile_height: 32, + columns: 16, + } + } + + async fn tick(&mut self) { + clear_background(BLACK); + self.resources.insert(get_last_key_pressed()); + self.resources + .insert(Point::from_tuple(mouse_tile_position())); + let current_state = self.resources.get::().unwrap().clone(); + match current_state { + TurnState::AwaitingInput => self + .input_systems + .execute(&mut self.ecs, &mut self.resources), + TurnState::PlayerTurn => self + .player_systems + .execute(&mut self.ecs, &mut self.resources), + TurnState::MonsterTurn => self + .monster_systems + .execute(&mut self.ecs, &mut self.resources), + TurnState::GameOver => self.game_over().await, + TurnState::Victory => self.victory().await, + TurnState::NextLevel => self.advance_level().await, + } + } + + async fn game_over(&mut self) { + print_color_centered(2, "Your quest has ended.", RED); + print_color_centered( + 4, + "Slain by a monster, your hero's journey has come to a \ + premature end.", + WHITE, + ); + print_color_centered( + 5, + "The Amulet of YALA remains unclaimed, and your home town \ + is not saved.", + WHITE, + ); + print_color_centered( + 8, + "Don't worry, you can always try again with a new hero.", + YELLOW, + ); + print_color_centered(9, "Press 1 to play again.", GREEN); + + if is_key_down(KeyCode::Key1) { + self.reset_game_state().await; + } + } + + async fn victory(&mut self) { + print_color_centered(2, "You have won!", GREEN); + print_color_centered( + 4, + "You put on the Amulet of YALA and feel its power course through \ + your veins.", + WHITE, + ); + print_color_centered( + 5, + "Your town is saved, and you can return to your normal life.", + WHITE, + ); + print_color_centered(7, "Press 1 to play again.", GREEN); + + if is_key_down(KeyCode::Key1) { + self.reset_game_state().await; + } + } + + async fn advance_level(&mut self) { + let player_entity = *::query() + .filter(component::()) + .iter(&mut self.ecs) + .nth(0) + .unwrap(); + + use std::collections::HashSet; + let mut entities_to_keep = HashSet::new(); + entities_to_keep.insert(player_entity); + + <(Entity, &Carried)>::query() + .iter(&self.ecs) + .filter(|(_e, carry)| carry.0 == player_entity) + .map(|(e, _carry)| *e) + .for_each(|e| { + entities_to_keep.insert(e); + }); + + let mut cb = CommandBuffer::new(&mut self.ecs); + for e in Entity::query().iter(&self.ecs) { + if !entities_to_keep.contains(e) { + cb.remove(*e); + } + } + cb.flush(&mut self.ecs); + + <&mut FieldOfView>::query() + .iter_mut(&mut self.ecs) + .for_each(|fov| fov.is_dirty = true); + + let mut map_builder = MapBuilder::new(); + let mut map_level = 0; + <(&mut Player, &mut Point)>::query() + .iter_mut(&mut self.ecs) + .for_each(|(player, pos)| { + player.map_level += 1; + map_level = player.map_level; + pos.x = map_builder.player_start.x; + pos.y = map_builder.player_start.y; + }); + + if map_level == 2 { + spawn_amulet_of_yala(&mut self.ecs, map_builder.amulet_start); + } else { + let exit_idx = map_builder.map.point2d_to_index(map_builder.amulet_start); + map_builder.map.tiles[exit_idx] = TileType::Exit; + } + + spawn_level( + &mut self.ecs, + map_level as usize, + &map_builder.monster_spawns, + ) + .await; + self.resources.insert(map_builder.map); + self.resources + .insert(CameraView::new(map_builder.player_start)); + let tileset = Self::tileset(self.texture); + self.resources.insert(tileset); + self.resources.insert(TurnState::AwaitingInput); + self.resources.insert(map_builder.theme); + } + + async fn reset_game_state(&mut self) { + self.ecs = World::default(); + self.resources = Resources::default(); + let mut map_builder = MapBuilder::new(); + let tileset = Self::tileset(self.texture); + spawn_player(&mut self.ecs, map_builder.player_start); + let exit_idx = map_builder.map.point2d_to_index(map_builder.amulet_start); + map_builder.map.tiles[exit_idx] = TileType::Exit; + spawn_level(&mut self.ecs, 0, &map_builder.monster_spawns).await; + self.resources.insert(map_builder.map); + self.resources + .insert(CameraView::new(map_builder.player_start)); + self.resources.insert(tileset); + self.resources.insert(TurnState::AwaitingInput); + self.resources.insert(map_builder.theme); + } +} + +fn window_conf() -> Conf { + Conf { + window_title: "Rusty Dungeon".to_owned(), + fullscreen: true, + ..Default::default() + } +} + +#[macroquad::main(window_conf())] +async fn main() { + let tileset = load_texture("assets/dungeonfont.png") + .await + .expect("Tile texture not found"); + tileset.set_filter(FilterMode::Nearest); + + let mut game = State::new(tileset).await; + loop { + game.tick().await; + next_frame().await + } +} diff --git a/rusty_roguelike-macroquad/src/map.rs b/rusty_roguelike-macroquad/src/map.rs new file mode 100644 index 000000000..2d98043f2 --- /dev/null +++ b/rusty_roguelike-macroquad/src/map.rs @@ -0,0 +1,100 @@ +use crate::prelude::*; + +const NUM_TILES: usize = (SCREEN_WIDTH * SCREEN_HEIGHT) as usize; + +#[derive(Copy, Clone, PartialEq)] +pub enum TileType { + Wall, + Floor, + Exit, +} + +pub fn map_idx(x: i32, y: i32) -> usize { + ((y * SCREEN_WIDTH) + x) as usize +} + +pub struct Map { + pub tiles: Vec, + pub revealed_tiles: Vec, +} + +impl Map { + pub fn new() -> Self { + Self { + tiles: vec![TileType::Floor; NUM_TILES], + revealed_tiles: vec![false; NUM_TILES], + } + } + + 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 + || self.tiles[map_idx(point.x, point.y)] == TileType::Exit) + } + + pub fn try_idx(&self, point: Point) -> Option { + if !self.in_bounds(point) { + None + } else { + Some(map_idx(point.x, point.y)) + } + } + + fn valid_exit(&self, loc: Point, delta: Point) -> Option { + let destination = loc + delta; + if self.in_bounds(destination) { + if self.can_enter_tile(destination) { + let idx = self.point2d_to_index(destination); + Some(idx) + } else { + None + } + } else { + None + } + } +} + +impl Algorithm2D for Map { + fn dimensions(&self) -> Point { + Point::new(SCREEN_WIDTH, SCREEN_HEIGHT) + } + + fn in_bounds(&self, point: Point) -> bool { + self.in_bounds(point) + } +} + +impl BaseMap for Map { + fn is_opaque(&self, idx: usize) -> bool { + self.tiles[idx as usize] != TileType::Floor + } + + fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> { + let mut exits = SmallVec::new(); + let location = self.index_to_point2d(idx); + + if let Some(idx) = self.valid_exit(location, Point::new(-1, 0)) { + exits.push((idx, 1.0)) + } + if let Some(idx) = self.valid_exit(location, Point::new(1, 0)) { + exits.push((idx, 1.0)) + } + if let Some(idx) = self.valid_exit(location, Point::new(0, -1)) { + exits.push((idx, 1.0)) + } + if let Some(idx) = self.valid_exit(location, Point::new(0, 1)) { + exits.push((idx, 1.0)) + } + + exits + } + + fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 { + DistanceAlg::Pythagoras.distance2d(self.index_to_point2d(idx1), self.index_to_point2d(idx2)) + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/automata.rs b/rusty_roguelike-macroquad/src/map_builder/automata.rs new file mode 100644 index 000000000..d2b94f75e --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/automata.rs @@ -0,0 +1,88 @@ +use super::MapArchitect; +use crate::prelude::*; + +pub struct CellularAutomataArchitect {} + +impl MapArchitect for CellularAutomataArchitect { + fn new(&mut self) -> MapBuilder { + let mut mb = MapBuilder { + map: Map::new(), + rooms: Vec::new(), + monster_spawns: Vec::new(), + player_start: Point::zero(), + amulet_start: Point::zero(), + theme: super::themes::DungeonTheme::new(), + }; + self.random_noise_map(&mut mb.map); + for _ in 0..10 { + self.iteration(&mut mb.map); + } + mb.add_boundaries(); + let start = self.find_start(&mb.map); + mb.monster_spawns = mb.spawn_monsters(&start); + mb.player_start = start; + mb.amulet_start = mb.find_most_distant(); + mb + } +} + +impl CellularAutomataArchitect { + fn random_noise_map(&mut self, map: &mut Map) { + map.tiles.iter_mut().for_each(|t| { + let roll = rand::gen_range(0, 100); + if roll > 55 { + *t = TileType::Floor; + } else { + *t = TileType::Wall; + } + }); + } + + fn count_neighbors(&self, x: i32, y: i32, map: &Map) -> usize { + let mut neighbors = 0; + for iy in -1..=1 { + for ix in -1..=1 { + if !(ix == 0 && iy == 0) && map.tiles[map_idx(x + ix, y + iy)] == TileType::Wall { + neighbors += 1; + } + } + } + + neighbors + } + + fn iteration(&mut self, map: &mut Map) { + let mut new_tiles = map.tiles.clone(); + for y in 1..SCREEN_HEIGHT - 1 { + for x in 1..SCREEN_WIDTH - 1 { + let neighbors = self.count_neighbors(x, y, map); + let idx = map_idx(x, y); + if neighbors > 4 || neighbors == 0 { + new_tiles[idx] = TileType::Wall; + } else { + new_tiles[idx] = TileType::Floor; + } + } + } + map.tiles = new_tiles; + } + + fn find_start(&self, map: &Map) -> Point { + let center = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); + let closest_point = map + .tiles + .iter() + .enumerate() + .filter(|(_, t)| **t == TileType::Floor) + .map(|(idx, _)| { + ( + idx, + DistanceAlg::Pythagoras.distance2d(center, map.index_to_point2d(idx)), + ) + }) + .min_by(|(_, distance), (_, distance2)| distance.partial_cmp(&distance2).unwrap()) + .map(|(idx, _)| idx) + .unwrap(); + map.index_to_point2d(closest_point) + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/drunkard.rs b/rusty_roguelike-macroquad/src/map_builder/drunkard.rs new file mode 100644 index 000000000..cc75d7445 --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/drunkard.rs @@ -0,0 +1,87 @@ +use super::MapArchitect; +use crate::prelude::*; + +const STAGGER_DISTANCE: usize = 400; +const NUM_TILES: usize = (SCREEN_WIDTH * SCREEN_HEIGHT) as usize; +const DESIRED_FLOOR: usize = NUM_TILES / 3; + +pub struct DrunkardsWalkArchitect {} + +impl MapArchitect for DrunkardsWalkArchitect { + fn new(&mut self) -> MapBuilder { + let mut mb = MapBuilder { + map: Map::new(), + rooms: Vec::new(), + monster_spawns: Vec::new(), + player_start: Point::zero(), + amulet_start: Point::zero(), + theme: super::themes::DungeonTheme::new(), + }; + + mb.fill(TileType::Wall); + let center = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); + self.drunkard(¢er, &mut mb.map); + while mb + .map + .tiles + .iter() + .filter(|t| **t == TileType::Floor) + .count() + < DESIRED_FLOOR + { + self.drunkard( + &Point::new( + rand::gen_range(0, SCREEN_WIDTH), + rand::gen_range(0, SCREEN_HEIGHT), + ), + &mut mb.map, + ); + let dijkstra_map = DijkstraMap::new( + SCREEN_WIDTH, + SCREEN_HEIGHT, + &vec![mb.map.point2d_to_index(center)], + &mb.map, + 1024.0, + ); + dijkstra_map + .map + .iter() + .enumerate() + .filter(|(_, distance)| *distance > &2000.0) + .for_each(|(idx, _)| mb.map.tiles[idx] = TileType::Wall); + } + mb.add_boundaries(); + mb.monster_spawns = mb.spawn_monsters(¢er); + mb.player_start = center; + mb.amulet_start = mb.find_most_distant(); + + mb + } +} + +impl DrunkardsWalkArchitect { + fn drunkard(&mut self, start: &Point, map: &mut Map) { + let mut drunkard_pos = start.clone(); + let mut distance_staggered = 0; + + loop { + let drunk_idx = map.point2d_to_index(drunkard_pos); + map.tiles[drunk_idx] = TileType::Floor; + + match rand::gen_range(0, 4) { + 0 => drunkard_pos.x -= 1, + 1 => drunkard_pos.x += 1, + 2 => drunkard_pos.y -= 1, + _ => drunkard_pos.y += 1, + } + if !map.in_bounds(drunkard_pos) { + break; + } + + distance_staggered += 1; + if distance_staggered > STAGGER_DISTANCE { + break; + } + } + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/empty.rs b/rusty_roguelike-macroquad/src/map_builder/empty.rs new file mode 100644 index 000000000..88bcf39cb --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/empty.rs @@ -0,0 +1,27 @@ +use super::MapArchitect; +use crate::prelude::*; + +pub struct EmptyArchitect {} + +impl MapArchitect for EmptyArchitect { + fn new(&mut self) -> MapBuilder { + let mut mb = MapBuilder { + map: Map::new(), + rooms: Vec::new(), + monster_spawns: Vec::new(), + player_start: Point::zero(), + amulet_start: Point::zero(), + theme: super::themes::DungeonTheme::new(), + }; + mb.fill(TileType::Floor); + mb.player_start = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); + mb.amulet_start = mb.find_most_distant(); + for _ in 0..50 { + mb.monster_spawns.push(Point::new( + rand::gen_range(1, SCREEN_WIDTH), + rand::gen_range(1, SCREEN_HEIGHT), + )) + } + mb + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/mod.rs b/rusty_roguelike-macroquad/src/map_builder/mod.rs new file mode 100644 index 000000000..2aa17c7ce --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/mod.rs @@ -0,0 +1,173 @@ +mod automata; +mod drunkard; +mod empty; +mod prefab; +mod rooms; +mod themes; + +use crate::prelude::*; +use automata::CellularAutomataArchitect; +use drunkard::DrunkardsWalkArchitect; +use prefab::apply_prefab; +use rooms::RoomsArchitect; +use themes::*; + +const NUM_ROOMS: usize = 20; + +pub trait MapTheme: Sync + Send { + fn tile_to_render(&self, tile_type: TileType, idx: usize) -> Sprite; +} + +trait MapArchitect { + fn new(&mut self) -> MapBuilder; +} + +pub struct MapBuilder { + pub map: Map, + pub rooms: Vec, + pub monster_spawns: Vec, + pub player_start: Point, + pub amulet_start: Point, + pub theme: Box, +} + +impl MapBuilder { + pub fn new() -> Self { + let mut architect: Box = match rand::gen_range(0, 3) { + 0 => Box::new(DrunkardsWalkArchitect {}), + 1 => Box::new(RoomsArchitect {}), + _ => Box::new(CellularAutomataArchitect {}), + }; + let mut mb = architect.new(); + apply_prefab(&mut mb); + + mb.theme = match rand::gen_range(0, 2) { + 0 => DungeonTheme::new(), + _ => ForestTheme::new(), + }; + + mb + } + + pub fn fill(&mut self, tile: TileType) { + self.map.tiles.iter_mut().for_each(|t| *t = tile); + } + + fn find_most_distant(&self) -> Point { + let dijkstra_map = DijkstraMap::new( + SCREEN_WIDTH, + SCREEN_HEIGHT, + &vec![self.map.point2d_to_index(self.player_start)], + &self.map, + 1024.0, + ); + const UNREACHABLE: &f32 = &f32::MAX; + self.map.index_to_point2d( + dijkstra_map + .map + .iter() + .enumerate() + .filter(|(_, dist)| *dist < UNREACHABLE) + .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) + .unwrap() + .0, + ) + } + + fn add_boundaries(&mut self) { + for x in 1..SCREEN_WIDTH { + self.map.tiles[map_idx(x, 1)] = TileType::Wall; + self.map.tiles[map_idx(x, SCREEN_HEIGHT - 1)] = TileType::Wall; + } + for y in 1..SCREEN_HEIGHT { + self.map.tiles[map_idx(1, y)] = TileType::Wall; + self.map.tiles[map_idx(SCREEN_WIDTH - 1, y)] = TileType::Wall; + } + } + + pub fn build_random_rooms(&mut self) { + while self.rooms.len() < NUM_ROOMS { + let room = Rect::with_size( + rand::gen_range(1, SCREEN_WIDTH - 10), + rand::gen_range(1, SCREEN_HEIGHT - 10), + rand::gen_range(2, 10), + rand::gen_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] = 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] = TileType::Floor; + } + } + } + + fn build_corridors(&mut self) { + 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 rand::gen_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); + } + } + } + + fn spawn_monsters(&self, start: &Point) -> Vec { + const NUM_MONSTERS: usize = 50; + let mut spawnable_tiles: Vec = self + .map + .tiles + .iter() + .enumerate() + .filter(|(idx, t)| { + **t == TileType::Floor + && DistanceAlg::Pythagoras.distance2d(*start, self.map.index_to_point2d(*idx)) + > 10.0 + }) + .map(|(idx, _)| self.map.index_to_point2d(idx)) + .collect(); + + let mut spawns = Vec::new(); + for _ in 0..NUM_MONSTERS { + let target_index = random_slice_index(&spawnable_tiles).unwrap(); + spawns.push(spawnable_tiles[target_index].clone()); + spawnable_tiles.remove(target_index); + } + spawns + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/prefab.rs b/rusty_roguelike-macroquad/src/map_builder/prefab.rs new file mode 100644 index 000000000..26cd2240e --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/prefab.rs @@ -0,0 +1,82 @@ +use crate::prelude::*; + +const FORTRESS: (&str, i32, i32) = ( + " +------------ +---######--- +---#----#--- +---#-M--#--- +-###----###- +--M------M-- +-###----###- +---#----#--- +---#----#--- +---######--- +------------ +", + 12, + 11, +); + +pub fn apply_prefab(mb: &mut MapBuilder) { + let mut placement = None; + let dijkstra_map = DijkstraMap::new( + SCREEN_WIDTH, + SCREEN_HEIGHT, + &vec![mb.map.point2d_to_index(mb.player_start)], + &mb.map, + 1024.0, + ); + + let mut attempts = 0; + while placement.is_none() && attempts < 10 { + let dimensions = Rect::with_size( + rand::gen_range(0, SCREEN_WIDTH - FORTRESS.1), + rand::gen_range(0, SCREEN_HEIGHT - FORTRESS.2), + FORTRESS.1, + FORTRESS.2, + ); + + let mut can_place = false; + dimensions.for_each(|pt| { + let idx = mb.map.point2d_to_index(pt); + let distance = dijkstra_map.map[idx]; + if distance < 2000.0 && distance > 20.0 && mb.amulet_start != pt { + can_place = true; + } + }); + + if can_place { + placement = Some(Point::new(dimensions.x1, dimensions.y1)); + let points = dimensions.point_set(); + mb.monster_spawns.retain(|pt| !points.contains(pt)); + } + attempts += 1; + } + + if let Some(placement) = placement { + debug!("Prefab placment: {:?}", placement); + let string_vec: Vec = FORTRESS + .0 + .chars() + .filter(|a| *a != '\r' && *a != '\n') + .collect(); + let mut i = 0; + for ty in placement.y..placement.y + FORTRESS.2 { + for tx in placement.x..placement.x + FORTRESS.1 { + let idx = map_idx(tx, ty); + let c = string_vec[i]; + match c { + 'M' => { + mb.map.tiles[idx] = TileType::Floor; + mb.monster_spawns.push(Point::new(tx, ty)); + } + '-' => mb.map.tiles[idx] = TileType::Floor, + '#' => mb.map.tiles[idx] = TileType::Wall, + _ => debug!("No idea what to do with [{}]", c), + } + i += 1; + } + } + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/rooms.rs b/rusty_roguelike-macroquad/src/map_builder/rooms.rs new file mode 100644 index 000000000..af44b3fe2 --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/rooms.rs @@ -0,0 +1,28 @@ +use super::MapArchitect; +use crate::prelude::*; + +pub struct RoomsArchitect {} + +impl MapArchitect for RoomsArchitect { + fn new(&mut self) -> MapBuilder { + let mut mb = MapBuilder { + map: Map::new(), + rooms: Vec::new(), + monster_spawns: Vec::new(), + player_start: Point::zero(), + amulet_start: Point::zero(), + theme: super::themes::DungeonTheme::new(), + }; + + mb.fill(TileType::Wall); + mb.build_random_rooms(); + mb.build_corridors(); + mb.player_start = mb.rooms[0].center(); + mb.amulet_start = mb.find_most_distant(); + for room in mb.rooms.iter().skip(1) { + mb.monster_spawns.push(room.center()); + } + + mb + } +} diff --git a/rusty_roguelike-macroquad/src/map_builder/themes.rs b/rusty_roguelike-macroquad/src/map_builder/themes.rs new file mode 100644 index 000000000..354d32017 --- /dev/null +++ b/rusty_roguelike-macroquad/src/map_builder/themes.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; + +pub struct DungeonTheme {} + +impl DungeonTheme { + pub fn new() -> Box { + Box::new(Self {}) + } +} + +impl MapTheme for DungeonTheme { + fn tile_to_render(&self, tile_type: TileType, idx: usize) -> Sprite { + match tile_type { + TileType::Floor => RANDOM_FLOOR_TILES[idx], + TileType::Wall => TileSet::SPRITE_WALL, + TileType::Exit => TileSet::SPRITE_STAIRS, + } + } +} + +pub struct ForestTheme {} + +impl MapTheme for ForestTheme { + fn tile_to_render(&self, tile_type: TileType, idx: usize) -> Sprite { + match tile_type { + TileType::Floor => TileSet::SPRITE_GROUND, + TileType::Wall => RANDOM_TREE_TILES[idx], + TileType::Exit => TileSet::SPRITE_STAIRS, + } + } +} + +impl ForestTheme { + pub fn new() -> Box { + Box::new(Self {}) + } +} diff --git a/rusty_roguelike-macroquad/src/spawner/mod.rs b/rusty_roguelike-macroquad/src/spawner/mod.rs new file mode 100644 index 000000000..e69c995e9 --- /dev/null +++ b/rusty_roguelike-macroquad/src/spawner/mod.rs @@ -0,0 +1,39 @@ +mod template; + +use crate::prelude::*; +use template::Templates; + +pub fn spawn_player(ecs: &mut World, pos: Point) { + ecs.push(( + Player { map_level: 0 }, + pos, + Render { + color: WHITE, + sprite: TileSet::SPRITE_PLAYER, + }, + Health { + current: 10, + max: 10, + }, + FieldOfView::new(8), + Damage(1), + )); +} + +pub fn spawn_amulet_of_yala(ecs: &mut World, pos: Point) { + ecs.push(( + Item, + AmuletOfYala, + pos, + Render { + color: WHITE, + sprite: TileSet::SPRITE_AMULET, + }, + Name("Amulet of Yala".to_string()), + )); +} + +pub async fn spawn_level(ecs: &mut World, level: usize, spawn_points: &[Point]) { + let template = Templates::load().await; + template.spawn_entities(ecs, level, spawn_points); +} diff --git a/rusty_roguelike-macroquad/src/spawner/template.rs b/rusty_roguelike-macroquad/src/spawner/template.rs new file mode 100644 index 000000000..e7ae9510d --- /dev/null +++ b/rusty_roguelike-macroquad/src/spawner/template.rs @@ -0,0 +1,108 @@ +use crate::prelude::*; +use legion::systems::CommandBuffer; +use macroquad::rand::ChooseRandom; +use nanoserde::DeRon; + +#[derive(Clone, Debug, DeRon)] +pub struct Template { + pub entity_type: EntityType, + pub levels: Vec, + pub frequency: i32, + pub name: String, + pub sprite: Sprite, + pub provides: Option>, + pub hp: Option, + pub base_damage: Option, +} + +#[derive(Clone, DeRon, Debug, PartialEq)] +pub enum EntityType { + Enemy, + Item, +} + +#[derive(Clone, DeRon, Debug)] +pub struct Templates { + pub entities: Vec