diff --git a/src/components/mod.rs b/src/components/mod.rs index 122adb4..1087442 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,9 +1,11 @@ -pub use left_mover::LeftMover; -pub use player::Player; -pub use position::Position; -pub use renderable::Renderable; - pub mod left_mover; +pub mod monster; pub mod player; pub mod position; pub mod renderable; + +pub use left_mover::LeftMover; +pub use monster::Monster; +pub use player::Player; +pub use position::Position; +pub use renderable::Renderable; diff --git a/src/components/monster.rs b/src/components/monster.rs new file mode 100644 index 0000000..98da013 --- /dev/null +++ b/src/components/monster.rs @@ -0,0 +1,41 @@ +use crate::vga_color as vga; +use bracket_lib::prelude::*; +use specs::prelude::*; +use specs_derive::Component; + +#[derive(Component, Debug)] +pub struct Monster { + pub kind: MonsterKind, + pub ticks_until_move: i32, +} + +#[derive(Clone, Copy, Debug)] +pub enum MonsterKind { + Slow, + Medium, + Fast, +} + +pub fn glyphs_for_kind(kind: MonsterKind) -> Vec { + match kind { + MonsterKind::Slow => vec![to_cp437('Ä'), to_cp437('A')], + MonsterKind::Medium => vec![to_cp437('ö'), to_cp437('Ö')], + MonsterKind::Fast => vec![to_cp437('Ω')], + } +} + +pub fn color_for_kind(kind: MonsterKind) -> RGB { + match kind { + MonsterKind::Slow => RGB::named(vga::RED_BRIGHT), + MonsterKind::Medium => RGB::named(vga::GREEN_BRIGHT), + MonsterKind::Fast => RGB::named(vga::BLUE_BRIGHT), + } +} + +pub fn ticks_for_kind(kind: MonsterKind) -> i32 { + match kind { + MonsterKind::Slow => 10, + MonsterKind::Medium => 8, + MonsterKind::Fast => 6, + } +} diff --git a/src/constants.rs b/src/constants.rs index fa78f61..c4028d7 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,8 @@ +/// Determined from trial and error, comparing to the original game in DOSBox, +/// Fast PC mode at 400 cycles/ms. +pub const CLOCK_PERIOD: f32 = 0.184; +pub const PLAYER_STEP_PERIOD: f32 = 1.0 / 7.5; + pub const SIDEBAR_POS_X: i32 = 66; pub const SIDEBAR_POS_Y: i32 = 0; diff --git a/src/main.rs b/src/main.rs index 4ddb18b..eee9d3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use bracket_lib::prelude::*; -use components::{LeftMover, Player, Position, Renderable}; -use constants::{MAP_X, MAP_Y}; -use map::Map; -use resources::LevelNumber; -use sound::SoundSystem; -use sound_effects::SoundEffects; -use specs::prelude::*; -use std::time::{Duration, Instant}; -use systems::LeftWalker; -use vga_color as vga; - pub mod components; pub mod constants; mod map; @@ -22,6 +10,21 @@ mod sound_effects; pub mod systems; pub mod vga_color; +use bracket_lib::prelude::*; +use components::{ + monster::{color_for_kind, glyphs_for_kind, ticks_for_kind, MonsterKind}, + LeftMover, Monster, Player, Position, Renderable, +}; +use constants::*; +use map::{Map, TileType}; +use resources::{Clock, LevelNumber, ShowDebugInfo}; +use sound::SoundSystem; +use sound_effects::SoundEffects; +use specs::prelude::*; +use std::time::{Duration, Instant}; +use systems::{LeftWalker, MonsterMotion}; +use vga_color as vga; + struct State { ecs: World, sound_system: SoundSystem, @@ -34,7 +37,7 @@ fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World, sound_system: &m for (player, pos) in (&mut players, &mut positions).join() { let now = Instant::now(); - if now - player.last_moved > Duration::from_secs_f32(0.15) { + if now - player.last_moved > Duration::from_secs_f32(PLAYER_STEP_PERIOD) { let destination = Point { x: pos.x + delta_x, y: pos.y + delta_y, @@ -43,13 +46,19 @@ fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World, sound_system: &m let mut sound_effects = ecs.fetch_mut::(); if map.in_bounds(destination) { - if map.can_player_enter(destination) { + if map.is_solid(destination) { + sound_system.play_sound(sound_effects.blocked.clone()); + } else { pos.x = destination.x; pos.y = destination.y; + let mut player_pos = ecs.write_resource::(); + player_pos.x = pos.x; + player_pos.y = pos.y; + + ecs.write_resource::().force_tick(); + sound_system.play_sound(sound_effects.step.clone()); - } else { - sound_system.play_sound(sound_effects.blocked.clone()); } } else { let static_sound = sound_effects.get_new_static_effect(sound_system); @@ -94,6 +103,10 @@ fn player_input(gs: &mut State, ctx: &mut BTerm) { let sound_effects = gs.ecs.fetch::(); gs.sound_system.play_sound(sound_effects.pickup.clone()); } + VirtualKeyCode::D => { + let mut show_debug_info = gs.ecs.write_resource::(); + show_debug_info.0 = !show_debug_info.0; + } VirtualKeyCode::Escape => { ctx.quit(); } @@ -136,7 +149,10 @@ impl State { fn run_systems(&mut self) { let mut lw = LeftWalker {}; lw.run_now(&self.ecs); + let mut mm = MonsterMotion {}; + mm.run_now(&self.ecs); self.ecs.maintain(); + self.ecs.write_resource::().try_tick(); } } @@ -157,17 +173,54 @@ fn main() -> BError { }; gs.ecs.insert(LevelNumber(0)); + gs.ecs.insert(ShowDebugInfo(false)); + gs.ecs.insert(Clock { + last_ticked: Instant::now(), + has_ticked: false, + ticks: 0, + }); gs.ecs.insert(sound_effects); - gs.ecs.insert(Map::new()); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); + let map = Map::new(); + let mut rng = RandomNumberGenerator::new(); + for (i, tile) in &mut map.get_tiles().iter().enumerate() { + if rng.roll_dice(1, 16) < 2 && *tile == TileType::Floor { + let position = map.index_to_point2d(i); + let kind = MonsterKind::Slow; + gs.ecs + .create_entity() + .with(Position { + x: position.x, + y: position.y, + }) + .with(Renderable { + glyph: *rng.random_slice_entry(&glyphs_for_kind(kind)).unwrap(), + fg: color_for_kind(kind), + bg: RGB::named(vga::BLACK), + }) + .with(Monster { + kind, + ticks_until_move: ticks_for_kind(kind), + }) + .build(); + } + } + gs.ecs.insert(map); + + let player_start_pos = Point { x: 40, y: 22 }; + gs.ecs .create_entity() - .with(Position { x: 40, y: 22 }) + .with(Position { + x: player_start_pos.x, + y: player_start_pos.y, + }) .with(Renderable { glyph: to_cp437('☻'), fg: RGB::named(vga::YELLOW_BRIGHT), @@ -183,6 +236,9 @@ fn main() -> BError { }) .build(); + gs.ecs + .insert(Point::new(player_start_pos.x, player_start_pos.y)); + // for i in 0..10 { // gs.ecs // .create_entity() diff --git a/src/map.rs b/src/map.rs index 8d3dd23..f571a25 100644 --- a/src/map.rs +++ b/src/map.rs @@ -24,6 +24,12 @@ impl Algorithm2D for Map { } } +impl Default for Map { + fn default() -> Self { + Self::new() + } +} + impl Map { pub fn new() -> Self { let mut tiles = vec![TileType::Floor; MAP_SIZE]; @@ -40,6 +46,10 @@ impl Map { } } + pub fn get_tiles(&self) -> &Vec { + &self.tiles + } + pub fn draw(&self, ctx: &mut BTerm) { // Border ctx.fill_region( @@ -85,7 +95,7 @@ impl Map { self.tiles[self.point2d_to_index(point)] } - pub fn can_player_enter(&self, point: Point) -> bool { - self.tile_at(point) != TileType::Wall + pub fn is_solid(&self, point: Point) -> bool { + self.tile_at(point) == TileType::Wall } } diff --git a/src/resources/mod.rs b/src/resources/mod.rs index d5e89f8..e69b810 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -1,2 +1,34 @@ +use crate::constants::CLOCK_PERIOD; +use std::time::{Duration, Instant}; + #[derive(Default)] pub struct LevelNumber(pub u32); + +#[derive(Default)] +pub struct ShowDebugInfo(pub bool); + +pub struct Clock { + pub last_ticked: Instant, + pub has_ticked: bool, + pub ticks: u128, +} + +impl Clock { + pub fn try_tick(&mut self) { + if Instant::now() - self.last_ticked > Duration::from_secs_f32(CLOCK_PERIOD) { + self.tick(); + } else { + self.has_ticked = false; + } + } + + pub fn force_tick(&mut self) { + self.tick(); + } + + fn tick(&mut self) { + self.has_ticked = true; + self.last_ticked = Instant::now(); + self.ticks += 1; + } +} diff --git a/src/sidebar.rs b/src/sidebar.rs index 77269e4..895f541 100644 --- a/src/sidebar.rs +++ b/src/sidebar.rs @@ -1,4 +1,5 @@ use crate::constants::{SIDEBAR_POS_X, SIDEBAR_POS_Y}; +use crate::resources::{Clock, ShowDebugInfo}; use crate::vga_color as vga; use crate::{LevelNumber, Player}; use bracket_lib::prelude::*; @@ -95,11 +96,21 @@ pub fn draw(ecs: &World, ctx: &mut BTerm) { ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 23, "Save"); ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 24, "Restore"); - ctx.print_color_right( - SIDEBAR_POS_X + 14, - SIDEBAR_POS_Y, - RGB::named(vga::GREEN_BRIGHT), - RGB::named(vga::BLACK), - &format!("{}", ctx.fps), - ); + if ecs.read_resource::().0 { + ctx.print_color_right( + SIDEBAR_POS_X + 14, + SIDEBAR_POS_Y, + RGB::named(vga::GREEN_BRIGHT), + RGB::named(vga::BLACK), + &format!("{}", ctx.fps), + ); + + ctx.print_color( + 0, + 0, + RGB::named(vga::YELLOW_BRIGHT), + RGB::named(vga::BLACK), + &format!("{}", ecs.read_resource::().ticks), + ); + } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index d0768d7..0bf9542 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,3 +1,5 @@ -pub use left_walker::LeftWalker; - pub mod left_walker; +pub mod monster_motion; + +pub use left_walker::LeftWalker; +pub use monster_motion::MonsterMotion; diff --git a/src/systems/monster_motion.rs b/src/systems/monster_motion.rs new file mode 100644 index 0000000..899ac1b --- /dev/null +++ b/src/systems/monster_motion.rs @@ -0,0 +1,69 @@ +use crate::{ + components::{ + monster::{self, ticks_for_kind}, + Monster, Position, Renderable, + }, + map::Map, + resources::Clock, +}; +use bracket_lib::{prelude::*, random::RandomNumberGenerator}; +use specs::prelude::*; + +pub struct MonsterMotion {} + +#[allow(clippy::type_complexity)] +impl<'a> System<'a> for MonsterMotion { + type SystemData = ( + ReadExpect<'a, Clock>, + ReadExpect<'a, Point>, + ReadExpect<'a, Map>, + WriteStorage<'a, Monster>, + WriteStorage<'a, Position>, + WriteStorage<'a, Renderable>, + ); + + fn run( + &mut self, + (clock, player_pos, map, mut monster, mut position, mut renderable): Self::SystemData, + ) { + for (monster, position, renderable) in (&mut monster, &mut position, &mut renderable).join() + { + if clock.has_ticked { + monster.ticks_until_move -= 1; + if monster.ticks_until_move <= 0 { + // Change glyph + let mut rng = RandomNumberGenerator::new(); + if let Some(glyph) = + rng.random_slice_entry(&monster::glyphs_for_kind(monster.kind)) + { + renderable.glyph = *glyph; + } + + // Move monster + let (mut delta_x, mut delta_y) = (0, 0); + if player_pos.x < position.x { + delta_x = -1; + } + if player_pos.x > position.x { + delta_x = 1; + } + if player_pos.y < position.y { + delta_y = -1; + } + if player_pos.y > position.y { + delta_y = 1; + } + let destination = Point { + x: position.x + delta_x, + y: position.y + delta_y, + }; + if !map.is_solid(destination) { + position.x = destination.x; + position.y = destination.y; + } + monster.ticks_until_move = ticks_for_kind(monster.kind); + } + } + } + } +}