Cleanup code structure to match the end of the first section of the tutorial

This commit is contained in:
Timothy Warren 2021-11-15 13:55:31 -05:00
parent 416af96be3
commit ffc997ce20
10 changed files with 279 additions and 271 deletions

View File

@ -142,11 +142,9 @@ pub struct WantsToDropItem {
pub item: Entity, pub item: Entity,
} }
pub struct SerializeMe; #[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct WantsToRemoveItem {
#[derive(Component, Serialize, Deserialize, Clone)] pub item: Entity,
pub struct SerializationHelper {
pub map: crate::map::Map,
} }
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
@ -176,7 +174,13 @@ pub struct DefenseBonus {
pub defense: i32, pub defense: i32,
} }
#[derive(Component, Debug, ConvertSaveload, Clone)] // Serialization helper code. We need to implement ConvertSaveLoad for each type that contains an
pub struct WantsToRemoveItem { // Entity.
pub item: Entity,
pub struct SerializeMe;
// Special component that exists to help serialize the game data
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct SerializationHelper {
pub map: super::map::Map,
} }

View File

@ -4,9 +4,8 @@ pub struct GameLog {
impl GameLog { impl GameLog {
pub fn new<S: ToString>(first_entry: S) -> Self { pub fn new<S: ToString>(first_entry: S) -> Self {
let mut entries: Vec<String> = Vec::new(); GameLog {
entries.push(first_entry.to_string()); entries: vec![first_entry.to_string()],
}
GameLog { entries }
} }
} }

View File

@ -3,19 +3,6 @@ use crate::{game_log::GameLog, Equipped, Map, RunState, State};
use rltk::{Point, Rltk, VirtualKeyCode, RGB}; use rltk::{Point, Rltk, VirtualKeyCode, RGB};
use specs::prelude::*; use specs::prelude::*;
#[derive(PartialEq, Copy, Clone)]
pub enum MainMenuSelection {
NewGame,
LoadGame,
Quit,
}
#[derive(PartialEq, Copy, Clone)]
pub enum MainMenuResult {
NoSelection { selected: MainMenuSelection },
Selected { selected: MainMenuSelection },
}
pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { pub fn draw_ui(ecs: &World, ctx: &mut Rltk) {
ctx.draw_box( ctx.draw_box(
0, 0,
@ -359,6 +346,95 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option
} }
} }
pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<Equipped>();
let entities = gs.ecs.entities();
let inventory = (&backpack, &names)
.join()
.filter(|item| item.0.owner == *player_entity);
let count = inventory.count();
let mut y = (25 - (count / 2)) as i32;
ctx.draw_box(
15,
y - 2,
31,
(count + 3) as i32,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
);
ctx.print_color(
18,
y - 2,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
"Remove Which Item?",
);
ctx.print_color(
18,
y + count as i32 + 1,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
"ESCAPE to cancel",
);
let mut equippable: Vec<Entity> = Vec::new();
let mut j = 0;
for (entity, _pack, name) in (&entities, &backpack, &names)
.join()
.filter(|item| item.1.owner == *player_entity)
{
ctx.set(
17,
y,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
rltk::to_cp437('('),
);
ctx.set(
18,
y,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
97 + j as rltk::FontCharType,
);
ctx.set(
19,
y,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
rltk::to_cp437(')'),
);
ctx.print(21, y, &name.name.to_string());
equippable.push(entity);
y += 1;
j += 1;
}
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) => match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = rltk::letter_to_option(key);
if selection > -1 && selection < count as i32 {
return (
ItemMenuResult::Selected,
Some(equippable[selection as usize]),
);
}
(ItemMenuResult::NoResponse, None)
}
},
}
}
pub fn ranged_target( pub fn ranged_target(
gs: &mut State, gs: &mut State,
ctx: &mut Rltk, ctx: &mut Rltk,
@ -418,6 +494,19 @@ pub fn ranged_target(
(ItemMenuResult::NoResponse, None) (ItemMenuResult::NoResponse, None)
} }
#[derive(PartialEq, Copy, Clone)]
pub enum MainMenuSelection {
NewGame,
LoadGame,
Quit,
}
#[derive(PartialEq, Copy, Clone)]
pub enum MainMenuResult {
NoSelection { selected: MainMenuSelection },
Selected { selected: MainMenuSelection },
}
pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult { pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult {
let save_exists = crate::saveload_system::does_save_exist(); let save_exists = crate::saveload_system::does_save_exist();
let runstate = gs.ecs.fetch::<RunState>(); let runstate = gs.ecs.fetch::<RunState>();
@ -531,95 +620,6 @@ pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult {
} }
} }
pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let names = gs.ecs.read_storage::<Name>();
let backpack = gs.ecs.read_storage::<Equipped>();
let entities = gs.ecs.entities();
let inventory = (&backpack, &names)
.join()
.filter(|item| item.0.owner == *player_entity);
let count = inventory.count();
let mut y = (25 - (count / 2)) as i32;
ctx.draw_box(
15,
y - 2,
31,
(count + 3) as i32,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
);
ctx.print_color(
18,
y - 2,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
"Remove Which Item?",
);
ctx.print_color(
18,
y + count as i32 + 1,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
"ESCAPE to cancel",
);
let mut equippable: Vec<Entity> = Vec::new();
let mut j = 0;
for (entity, _pack, name) in (&entities, &backpack, &names)
.join()
.filter(|item| item.1.owner == *player_entity)
{
ctx.set(
17,
y,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
rltk::to_cp437('('),
);
ctx.set(
18,
y,
RGB::named(rltk::YELLOW),
RGB::named(rltk::BLACK),
97 + j as rltk::FontCharType,
);
ctx.set(
19,
y,
RGB::named(rltk::WHITE),
RGB::named(rltk::BLACK),
rltk::to_cp437(')'),
);
ctx.print(21, y, &name.name.to_string());
equippable.push(entity);
y += 1;
j += 1;
}
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) => match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = rltk::letter_to_option(key);
if selection > -1 && selection < count as i32 {
return (
ItemMenuResult::Selected,
Some(equippable[selection as usize]),
);
}
(ItemMenuResult::NoResponse, None)
}
},
}
}
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
pub enum GameOverResult { pub enum GameOverResult {
NoSelection, NoSelection,

View File

@ -65,6 +65,7 @@ impl<'a> System<'a> for ItemUseSystem {
WriteStorage<'a, InBackpack>, WriteStorage<'a, InBackpack>,
); );
#[allow(clippy::cognitive_complexity)]
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let ( let (
player_entity, player_entity,
@ -267,6 +268,7 @@ impl<'a> System<'a> for ItemUseSystem {
pub struct ItemDropSystem {} pub struct ItemDropSystem {}
impl<'a> System<'a> for ItemDropSystem { impl<'a> System<'a> for ItemDropSystem {
#[allow(clippy::type_complexity)]
type SystemData = ( type SystemData = (
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
WriteExpect<'a, GameLog>, WriteExpect<'a, GameLog>,
@ -323,6 +325,7 @@ impl<'a> System<'a> for ItemDropSystem {
pub struct ItemRemoveSystem {} pub struct ItemRemoveSystem {}
impl<'a> System<'a> for ItemRemoveSystem { impl<'a> System<'a> for ItemRemoveSystem {
#[allow(clippy::type_complexity)]
type SystemData = ( type SystemData = (
Entities<'a>, Entities<'a>,
WriteStorage<'a, WantsToRemoveItem>, WriteStorage<'a, WantsToRemoveItem>,

View File

@ -43,8 +43,6 @@ macro_rules! register {
} }
} }
pub const MAP_SIZE: usize = 80 * 50;
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
pub enum RunState { pub enum RunState {
AwaitingInput, AwaitingInput,
@ -213,6 +211,25 @@ impl GameState for State {
} }
} }
} }
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::<WantsToRemoveItem>();
intent
.insert(
*self.ecs.fetch::<Entity>(),
WantsToRemoveItem { item: item_entity },
)
.expect("Unable to insert intent to remove item");
newrunstate = RunState::PlayerTurn;
}
}
}
RunState::ShowTargeting { range, item } => { RunState::ShowTargeting { range, item } => {
let result = gui::ranged_target(self, ctx, range); let result = gui::ranged_target(self, ctx, range);
match result.0 { match result.0 {
@ -253,6 +270,15 @@ impl GameState for State {
} }
}, },
}, },
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 => { RunState::SaveGame => {
saveload_system::save_game(&mut self.ecs); saveload_system::save_game(&mut self.ecs);
@ -264,34 +290,6 @@ impl GameState for State {
self.goto_next_level(); self.goto_next_level();
newrunstate = RunState::PreRun; newrunstate = RunState::PreRun;
} }
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::<WantsToRemoveItem>();
intent
.insert(
*self.ecs.fetch::<Entity>(),
WantsToRemoveItem { item: item_entity },
)
.expect("Unable to insert intent to remove item");
newrunstate = RunState::PlayerTurn;
}
}
}
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,
};
}
},
} }
{ {
@ -504,7 +502,9 @@ fn main() -> rltk::BError {
gs.ecs.insert(map); gs.ecs.insert(map);
gs.ecs.insert(Point::new(player_x, player_y)); gs.ecs.insert(Point::new(player_x, player_y));
gs.ecs.insert(player_entity); gs.ecs.insert(player_entity);
gs.ecs.insert(RunState::PreRun); gs.ecs.insert(RunState::MainMenu {
menu_selection: gui::MainMenuSelection::NewGame,
});
gs.ecs.insert(GameLog::new("Welcome to Rusty Roguelike")); gs.ecs.insert(GameLog::new("Welcome to Rusty Roguelike"));
rltk::main_loop(context, gs) rltk::main_loop(context, gs)

View File

@ -65,6 +65,28 @@ impl Map {
} }
} }
fn is_exit_valid(&self, x: i32, y: i32) -> bool {
if x < 1 || x > self.width - 1 || y < 1 || y > self.height - 1 {
return false;
}
let idx = self.xy_idx(x, y);
!self.blocked[idx]
}
pub fn populate_blocked(&mut self) {
for (i, tile) in self.tiles.iter_mut().enumerate() {
self.blocked[i] = *tile == TileType::Wall;
}
}
pub fn clear_content_index(&mut self) {
for content in self.tile_content.iter_mut() {
content.clear();
}
}
/// Makes a new map using the algorithm from http://rogueliketutorials.com/tutorials/tcod/part-3/ /// Makes a new map using the algorithm from http://rogueliketutorials.com/tutorials/tcod/part-3/
/// This gives a handful of random rooms and corridors joining them together /// This gives a handful of random rooms and corridors joining them together
pub fn new_map_rooms_and_corridors(new_depth: i32) -> Map { pub fn new_map_rooms_and_corridors(new_depth: i32) -> Map {
@ -127,34 +149,6 @@ impl Map {
map map
} }
fn is_exit_valid(&self, x: i32, y: i32) -> bool {
if x < 1 || x > self.width - 1 || y < 1 || y > self.height - 1 {
return false;
}
let idx = self.xy_idx(x, y);
!self.blocked[idx]
}
pub fn populate_blocked(&mut self) {
for (i, tile) in self.tiles.iter_mut().enumerate() {
self.blocked[i] = *tile == TileType::Wall;
}
}
pub fn clear_content_index(&mut self) {
for content in self.tile_content.iter_mut() {
content.clear();
}
}
}
impl Algorithm2D for Map {
fn dimensions(&self) -> Point {
Point::new(self.width, self.height)
}
} }
impl BaseMap for Map { impl BaseMap for Map {
@ -208,6 +202,12 @@ impl BaseMap for Map {
} }
} }
impl Algorithm2D for Map {
fn dimensions(&self) -> Point {
Point::new(self.width, self.height)
}
}
pub fn draw_map(ecs: &World, ctx: &mut Rltk) { pub fn draw_map(ecs: &World, ctx: &mut Rltk) {
let map = ecs.fetch::<Map>(); let map = ecs.fetch::<Map>();

View File

@ -7,6 +7,7 @@ use specs::prelude::*;
pub struct MeleeCombatSystem {} pub struct MeleeCombatSystem {}
impl<'a> System<'a> for MeleeCombatSystem { impl<'a> System<'a> for MeleeCombatSystem {
#[allow(clippy::type_complexity)]
type SystemData = ( type SystemData = (
Entities<'a>, Entities<'a>,
WriteExpect<'a, GameLog>, WriteExpect<'a, GameLog>,

View File

@ -54,6 +54,86 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) {
} }
} }
pub fn try_next_level(ecs: &mut World) -> bool {
let player_pos = ecs.fetch::<Point>();
let map = ecs.fetch::<Map>();
let player_idx = map.xy_idx(player_pos.x, player_pos.y);
if map.tiles[player_idx] == TileType::DownStairs {
true
} else {
let mut gamelog = ecs.fetch_mut::<GameLog>();
gamelog
.entries
.push("There is no way down from here.".to_string());
false
}
}
fn get_item(ecs: &mut World) {
let player_pos = ecs.fetch::<Point>();
let player_entity = ecs.fetch::<Entity>();
let entities = ecs.entities();
let items = ecs.read_storage::<Item>();
let positions = ecs.read_storage::<Position>();
let mut gamelog = ecs.fetch_mut::<GameLog>();
let mut target_item: Option<Entity> = None;
for (item_entity, _item, position) in (&entities, &items, &positions).join() {
if position.x == player_pos.x && position.y == player_pos.y {
target_item = Some(item_entity);
}
}
match target_item {
None => gamelog
.entries
.push("There is nothing here to pick up.".to_string()),
Some(item) => {
let mut pickup = ecs.write_storage::<WantsToPickupItem>();
pickup
.insert(
*player_entity,
WantsToPickupItem {
collected_by: *player_entity,
item,
},
)
.expect("Unable to pick up item?!");
}
}
}
fn skip_turn(ecs: &mut World) -> RunState {
let player_entity = ecs.fetch::<Entity>();
let viewshed_components = ecs.read_storage::<Viewshed>();
let monsters = ecs.read_storage::<Monster>();
let worldmap_resource = ecs.fetch::<Map>();
let mut can_heal = true;
let viewshed = viewshed_components.get(*player_entity).unwrap();
for tile in viewshed.visible_tiles.iter() {
let idx = worldmap_resource.xy_idx(tile.x, tile.y);
for entity_id in worldmap_resource.tile_content[idx].iter() {
match monsters.get(*entity_id) {
None => {}
Some(_) => {
can_heal = false;
}
}
}
}
if can_heal {
let mut health_components = ecs.write_storage::<CombatStats>();
let player_hp = health_components.get_mut(*player_entity).unwrap();
player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp);
}
RunState::PlayerTurn
}
pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
// Player movement // Player movement
match ctx.key { match ctx.key {
@ -126,83 +206,3 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
RunState::PlayerTurn RunState::PlayerTurn
} }
fn get_item(ecs: &mut World) {
let player_pos = ecs.fetch::<Point>();
let player_entity = ecs.fetch::<Entity>();
let entities = ecs.entities();
let items = ecs.read_storage::<Item>();
let positions = ecs.read_storage::<Position>();
let mut gamelog = ecs.fetch_mut::<GameLog>();
let mut target_item: Option<Entity> = None;
for (item_entity, _item, position) in (&entities, &items, &positions).join() {
if position.x == player_pos.x && position.y == player_pos.y {
target_item = Some(item_entity);
}
}
match target_item {
None => gamelog
.entries
.push("There is nothing here to pick up.".to_string()),
Some(item) => {
let mut pickup = ecs.write_storage::<WantsToPickupItem>();
pickup
.insert(
*player_entity,
WantsToPickupItem {
collected_by: *player_entity,
item,
},
)
.expect("Unable to pick up item?!");
}
}
}
pub fn try_next_level(ecs: &mut World) -> bool {
let player_pos = ecs.fetch::<Point>();
let map = ecs.fetch::<Map>();
let player_idx = map.xy_idx(player_pos.x, player_pos.y);
if map.tiles[player_idx] == TileType::DownStairs {
true
} else {
let mut gamelog = ecs.fetch_mut::<GameLog>();
gamelog
.entries
.push("There is no way down from here.".to_string());
false
}
}
fn skip_turn(ecs: &mut World) -> RunState {
let player_entity = ecs.fetch::<Entity>();
let viewshed_components = ecs.read_storage::<Viewshed>();
let monsters = ecs.read_storage::<Monster>();
let worldmap_resource = ecs.fetch::<Map>();
let mut can_heal = true;
let viewshed = viewshed_components.get(*player_entity).unwrap();
for tile in viewshed.visible_tiles.iter() {
let idx = worldmap_resource.xy_idx(tile.x, tile.y);
for entity_id in worldmap_resource.tile_content[idx].iter() {
match monsters.get(*entity_id) {
None => {}
Some(_) => {
can_heal = false;
}
}
}
}
if can_heal {
let mut health_components = ecs.write_storage::<CombatStats>();
let player_hp = health_components.get_mut(*player_entity).unwrap();
player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp);
}
RunState::PlayerTurn
}

View File

@ -22,21 +22,6 @@ macro_rules! serialize_individually {
}; };
} }
macro_rules! deserialize_individually {
($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
$(
DeserializeComponents::<NoError, _>::deserialize(
&mut ( &mut $ecs.write_storage::<$type>(), ),
&mut $data.0, // entities
&mut $data.1, // marker
&mut $data.2, // allocater
&mut $de,
)
.unwrap();
)*
};
}
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn save_game(_ecs: &mut World) {} pub fn save_game(_ecs: &mut World) {}
@ -102,6 +87,21 @@ pub fn does_save_exist() -> bool {
Path::new("./savegame.json").exists() Path::new("./savegame.json").exists()
} }
macro_rules! deserialize_individually {
($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
$(
DeserializeComponents::<NoError, _>::deserialize(
&mut ( &mut $ecs.write_storage::<$type>(), ),
&mut $data.0, // entities
&mut $data.1, // marker
&mut $data.2, // allocater
&mut $de,
)
.unwrap();
)*
};
}
pub fn load_game(ecs: &mut World) { pub fn load_game(ecs: &mut World) {
{ {
// Delete everything // Delete everything
@ -183,7 +183,7 @@ pub fn load_game(ecs: &mut World) {
} }
ecs.delete_entity(deleteme.unwrap()) ecs.delete_entity(deleteme.unwrap())
.expect("Unable to delete helper entitiy"); .expect("Unable to delete helper entity");
} }
pub fn delete_save() { pub fn delete_save() {

View File

@ -3,7 +3,7 @@ use crate::components::{
Equippable, InflictsDamage, Item, MeleePowerBonus, Monster, Name, Player, Position, Equippable, InflictsDamage, Item, MeleePowerBonus, Monster, Name, Player, Position,
ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed, ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed,
}; };
use crate::{random_table::RandomTable, Rect, MAP_WIDTH}; use crate::{random_table::RandomTable, Rect, map::MAP_WIDTH};
use rltk::{RandomNumberGenerator, RGB}; use rltk::{RandomNumberGenerator, RGB};
use specs::prelude::*; use specs::prelude::*;
use specs::saveload::{MarkedBuilder, SimpleMarker}; use specs::saveload::{MarkedBuilder, SimpleMarker};
@ -146,6 +146,7 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) {
.with(Item {}) .with(Item {})
.with(Consumable {}) .with(Consumable {})
.with(ProvidesHealing { heal_amount: 8 }) .with(ProvidesHealing { heal_amount: 8 })
.marked::<SimpleMarker<SerializeMe>>()
.build(); .build();
} }
@ -162,7 +163,7 @@ fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {
.with(Item {}) .with(Item {})
.with(Consumable {}) .with(Consumable {})
.with(Ranged { range: 6 }) .with(Ranged { range: 6 })
.with(InflictsDamage { damage: 8 }) .with(InflictsDamage { damage: 20 })
.marked::<SimpleMarker<SerializeMe>>() .marked::<SimpleMarker<SerializeMe>>()
.build(); .build();
} }