use rltk::{GameState, Point, Rltk}; use specs::prelude::*; use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; mod components; mod damage_system; mod game_log; mod gui; mod hunger_system; mod inventory_system; mod map; mod map_indexing_system; mod melee_combat_system; mod monster_ai_system; mod particle_system; mod player; pub mod random_table; mod rect; mod rex_assets; pub mod saveload_system; mod spawner; mod trigger_system; mod visibility_system; use crate::inventory_system::ItemRemoveSystem; use components::*; use damage_system::DamageSystem; pub use game_log::GameLog; use inventory_system::{ItemCollectionSystem, ItemDropSystem, ItemUseSystem}; pub use map::*; use map_indexing_system::MapIndexingSystem; use melee_combat_system::MeleeCombatSystem; use monster_ai_system::MonsterAI; use player::*; pub use rect::Rect; use visibility_system::VisibilitySystem; /// Cut down on the amount of syntax to register components macro_rules! register { // $gs is needed to get the scope at the usage point // $Type is the Component type that is being registered ($gs: ident <- $( $Type: ty ),*,) => { $( $gs.ecs.register::<$Type>(); )* } } #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory, ShowDropItem, ShowTargeting { range: i32, item: Entity, }, MainMenu { menu_selection: gui::MainMenuSelection, }, SaveGame, NextLevel, ShowRemoveItem, GameOver, MagicMapReveal { row: i32, }, } pub struct State { pub ecs: World, } impl State { fn new() -> Self { State { ecs: World::new() } } fn run_systems(&mut self) { let mut vis = VisibilitySystem {}; vis.run_now(&self.ecs); let mut mob = MonsterAI {}; mob.run_now(&self.ecs); let mut triggers = trigger_system::TriggerSystem {}; triggers.run_now(&self.ecs); let mut mapindex = MapIndexingSystem {}; mapindex.run_now(&self.ecs); let mut melee = MeleeCombatSystem {}; melee.run_now(&self.ecs); let mut damage = DamageSystem {}; damage.run_now(&self.ecs); let mut pickup = ItemCollectionSystem {}; pickup.run_now(&self.ecs); let mut items = ItemUseSystem {}; items.run_now(&self.ecs); let mut drop_items = ItemDropSystem {}; drop_items.run_now(&self.ecs); let mut item_remove = ItemRemoveSystem {}; item_remove.run_now(&self.ecs); let mut hunger = hunger_system::HungerSystem {}; hunger.run_now(&self.ecs); let mut particles = particle_system::ParticleSpawnSystem {}; particles.run_now(&self.ecs); self.ecs.maintain(); } } impl GameState for State { fn tick(&mut self, ctx: &mut Rltk) { let mut newrunstate; { let runstate = self.ecs.fetch::(); newrunstate = *runstate; } ctx.cls(); particle_system::cull_dead_particles(&mut self.ecs, ctx); match newrunstate { RunState::MainMenu { .. } => {} RunState::GameOver { .. } => {} _ => { // Draw the UI draw_map(&self.ecs, ctx); { let positions = self.ecs.read_storage::(); let renderables = self.ecs.read_storage::(); let hidden = self.ecs.read_storage::(); let map = self.ecs.fetch::(); let mut data: Vec<_> = (&positions, &renderables, !&hidden).join().collect(); data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); for (pos, render, _hidden) in data.iter() { let idx = map.xy_idx(pos.x, pos.y); if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) } } gui::draw_ui(&self.ecs, ctx); } } } match newrunstate { RunState::PreRun => { self.run_systems(); self.ecs.maintain(); newrunstate = RunState::AwaitingInput; } RunState::AwaitingInput => { newrunstate = player_input(self, ctx); } RunState::PlayerTurn => { self.run_systems(); self.ecs.maintain(); match *self.ecs.fetch::() { RunState::MagicMapReveal { .. } => { newrunstate = RunState::MagicMapReveal { row: 0 } } _ => newrunstate = RunState::MonsterTurn, } } RunState::MonsterTurn => { self.run_systems(); self.ecs.maintain(); newrunstate = RunState::AwaitingInput; } RunState::ShowInventory => { let result = gui::show_inventory(self, ctx); match result.0 { gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); let is_ranged = self.ecs.read_storage::(); if let Some(is_item_ranged) = is_ranged.get(item_entity) { newrunstate = RunState::ShowTargeting { range: is_item_ranged.range, item: item_entity, }; } else { let mut intent = self.ecs.write_storage::(); intent .insert( *self.ecs.fetch::(), WantsToUseItem { item: item_entity, target: None, }, ) .expect("failed to add intent to use item"); newrunstate = RunState::PlayerTurn; } } } } RunState::ShowDropItem => { let result = gui::drop_item_menu(self, ctx); match result.0 { gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); let mut intent = self.ecs.write_storage::(); intent .insert( *self.ecs.fetch::(), WantsToDropItem { item: item_entity }, ) .expect("failed to add intent to drop item"); newrunstate = RunState::PlayerTurn; } } } RunState::ShowRemoveItem => { let result = gui::remove_item_menu(self, ctx); match result.0 { gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); let mut intent = self.ecs.write_storage::(); intent .insert( *self.ecs.fetch::(), WantsToRemoveItem { item: item_entity }, ) .expect("Unable to insert intent to remove item"); newrunstate = RunState::PlayerTurn; } } } RunState::ShowTargeting { range, item } => { let result = gui::ranged_target(self, ctx, range); match result.0 { gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let mut intent = self.ecs.write_storage::(); intent .insert( *self.ecs.fetch::(), WantsToUseItem { item, target: result.1, }, ) .expect("failed to add intent to use item"); newrunstate = RunState::PlayerTurn; } } } RunState::MainMenu { .. } => match gui::main_menu(self, ctx) { gui::MainMenuResult::NoSelection { selected } => { newrunstate = RunState::MainMenu { menu_selection: selected, } } gui::MainMenuResult::Selected { selected } => match selected { gui::MainMenuSelection::NewGame => newrunstate = RunState::PreRun, gui::MainMenuSelection::LoadGame => { saveload_system::load_game(&mut self.ecs); newrunstate = RunState::AwaitingInput; saveload_system::delete_save(); } gui::MainMenuSelection::Quit => { ::std::process::exit(0); } }, }, RunState::GameOver => match gui::game_over(ctx) { gui::GameOverResult::NoSelection => {} gui::GameOverResult::QuitToMenu => { self.game_over_cleanup(); newrunstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame, }; } }, RunState::SaveGame => { saveload_system::save_game(&mut self.ecs); newrunstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame, } } RunState::NextLevel => { self.goto_next_level(); newrunstate = RunState::PreRun; } RunState::MagicMapReveal { row } => { let mut map = self.ecs.fetch_mut::(); for x in 0..MAP_WIDTH { let idx = map.xy_idx(x as i32, row); map.revealed_tiles[idx] = true; } if row as usize == MAP_HEIGHT - 1 { newrunstate = RunState::MonsterTurn; } else { newrunstate = RunState::MagicMapReveal { row: row + 1 }; } } } { let mut runwriter = self.ecs.write_resource::(); *runwriter = newrunstate; } damage_system::delete_the_dead(&mut self.ecs); } } impl State { fn entities_to_remove_on_level_change(&mut self) -> Vec { let entities = self.ecs.entities(); let player = self.ecs.read_storage::(); let backpack = self.ecs.read_storage::(); let player_entity = self.ecs.fetch::(); let equipped = self.ecs.read_storage::(); let mut to_delete: Vec = Vec::new(); for entity in entities.join() { let mut should_delete = true; // Don't delete the player if let Some(_p) = player.get(entity) { should_delete = false; } // Don't delete the player's equipment if let Some(bp) = backpack.get(entity) { if bp.owner == *player_entity { should_delete = false; } } if let Some(eq) = equipped.get(entity) { if eq.owner == *player_entity { should_delete = false; } } if should_delete { to_delete.push(entity); } } to_delete } fn goto_next_level(&mut self) { // Delete entities that aren't the palyer or their equipment let to_delete = self.entities_to_remove_on_level_change(); for target in to_delete { self.ecs .delete_entity(target) .expect("failed to delete entity"); } // Build a new map and place the player let worldmap; let current_depth; { let mut worldmap_resource = self.ecs.write_resource::(); current_depth = worldmap_resource.depth; *worldmap_resource = Map::new_map_rooms_and_corridors(current_depth + 1); worldmap = worldmap_resource.clone(); } // Spawn bad guys for room in worldmap.rooms.iter().skip(1) { spawner::spawn_room(&mut self.ecs, room, current_depth + 1); } // Place the player and update resources let (player_x, player_y) = worldmap.rooms[0].center(); let mut player_position = self.ecs.write_resource::(); *player_position = Point::new(player_x, player_y); let mut position_components = self.ecs.write_storage::(); let player_entity = self.ecs.fetch::(); if let Some(player_pos_comp) = position_components.get_mut(*player_entity) { player_pos_comp.x = player_x; player_pos_comp.y = player_y; } // Mark the player's visibility as dirty let mut viewshed_components = self.ecs.write_storage::(); if let Some(vs) = viewshed_components.get_mut(*player_entity) { vs.dirty = true; } // Notify the player and give them some health let mut gamelog = self.ecs.fetch_mut::(); gamelog .entries .push("You descend to the next level, and take a moment to heal.".to_string()); let mut player_health_store = self.ecs.write_storage::(); let player_health = player_health_store.get_mut(*player_entity); if let Some(player_health) = player_health { player_health.hp = i32::max(player_health.hp, player_health.max_hp / 2); } } fn game_over_cleanup(&mut self) { // Delete everything let mut to_delete = Vec::new(); for e in self.ecs.entities().join() { to_delete.push(e); } for del in to_delete.iter() { self.ecs .delete_entity(*del) .expect("Failed to delete entity"); } // Build a new map and place the player let worldmap; { let mut worldmap_resource = self.ecs.write_resource::(); *worldmap_resource = Map::new_map_rooms_and_corridors(1); worldmap = worldmap_resource.clone(); } // Spawn bad guys for room in worldmap.rooms.iter().skip(1) { spawner::spawn_room(&mut self.ecs, room, 1); } // Place the player and update resources let (player_x, player_y) = worldmap.rooms[0].center(); let player_entity = spawner::player(&mut self.ecs, player_x, player_y); let mut player_position = self.ecs.write_resource::(); *player_position = Point::new(player_x, player_y); let mut position_components = self.ecs.write_storage::(); let mut player_entity_writer = self.ecs.write_resource::(); *player_entity_writer = player_entity; if let Some(player_pos_comp) = position_components.get_mut(player_entity) { player_pos_comp.x = player_x; player_pos_comp.y = player_y; } // Mark the player's visibility as dirty let mut viewshed_components = self.ecs.write_storage::(); let vs = viewshed_components.get_mut(player_entity); if let Some(vs) = vs { vs.dirty = true; } } } fn main() -> rltk::BError { use rltk::RltkBuilder; let context = RltkBuilder::simple80x50() .with_title("Roguelike Tutorial") .build()?; let mut gs = State::new(); register!( gs <- Position, Renderable, Player, Viewshed, Monster, Name, BlocksTile, CombatStats, WantsToMelee, SufferDamage, Item, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, WantsToDropItem, Consumable, Ranged, InflictsDamage, AreaOfEffect, Confusion, SimpleMarker, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden, EntryTrigger, EntityMoved, ); gs.ecs.insert(SimpleMarkerAllocator::::new()); let map = Map::new_map_rooms_and_corridors(1); let (player_x, player_y) = map.rooms[0].center(); let player_entity = spawner::player(&mut gs.ecs, player_x, player_y); gs.ecs.insert(rltk::RandomNumberGenerator::new()); for room in map.rooms.iter().skip(1) { spawner::spawn_room(&mut gs.ecs, room, 1); } gs.ecs.insert(map); gs.ecs.insert(Point::new(player_x, player_y)); gs.ecs.insert(player_entity); gs.ecs.insert(RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame, }); gs.ecs.insert(GameLog::new("Welcome to Rusty Roguelike")); gs.ecs.insert(particle_system::ParticleBuilder::new()); gs.ecs.insert(rex_assets::RexAssets::new()); rltk::main_loop(context, gs) }