diff --git a/Cargo.lock b/Cargo.lock index 3f85951..7319159 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,14 +22,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.7.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" [[package]] name = "aho-corasick" @@ -114,6 +109,7 @@ dependencies = [ "byteorder", "lazy_static", "parking_lot", + "serde", ] [[package]] @@ -122,6 +118,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4db22c32c68bd9330ab982f8ff7ffe7b10541d16ea7d7d51aac074499850402b" dependencies = [ + "serde", "ultraviolet", ] @@ -170,6 +167,7 @@ dependencies = [ "rand", "rand_xorshift", "regex", + "serde", ] [[package]] @@ -443,12 +441,13 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.3" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils 0.8.6", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -753,11 +752,12 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "96282e96bfcd3da0d3aa9938bedf1e50df3269b6db08b4876d2da0bb1a0841cf" dependencies = [ "ahash", + "autocfg", ] [[package]] @@ -822,6 +822,12 @@ dependencies = [ "libc", ] +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jni-sys" version = "0.3.0" @@ -924,6 +930,12 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.4.1" @@ -1367,6 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core", + "serde", ] [[package]] @@ -1463,6 +1476,8 @@ name = "roguelike" version = "0.1.0" dependencies = [ "rltk", + "serde", + "serde_json", "specs", "specs-derive", ] @@ -1477,6 +1492,12 @@ dependencies = [ "owned_ttf_parser", ] +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + [[package]] name = "safe_arch" version = "0.5.2" @@ -1515,9 +1536,34 @@ checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4" [[package]] name = "serde" -version = "1.0.135" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "shared_library" @@ -1531,9 +1577,9 @@ dependencies = [ [[package]] name = "shred" -version = "0.12.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0210289d693217926314867c807e0b7b42f7e23c136adb31f8697f5bf242d3" +checksum = "c5f08237e667ac94ad20f8878b5943d91a93ccb231428446c57c21c57779016d" dependencies = [ "arrayvec", "hashbrown", @@ -1591,15 +1637,16 @@ dependencies = [ [[package]] name = "specs" -version = "0.17.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcc1e4ba7ab1f08ecb3d7e2f693defc3907e2c03bb0924f9978be45b364f83f" +checksum = "fff28a29366aff703d5da8a7e2c8875dc8453ac1118f842cbc0fa70c7db51240" dependencies = [ "crossbeam-queue", "hashbrown", "hibitset", "log", "rayon", + "serde", "shred", "shrev", "tuple_utils", diff --git a/Cargo.toml b/Cargo.toml index 406cfc4..3b569e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rltk = {version = "0.8.0"} -specs = "0.17.0" -specs-derive = "0.4.1" +rltk = { version = "0.8.0", features = ["serde"] } +specs = { version = "0.16.1", features = ["serde"] } +specs-derive = { version = "0.4.1" } +serde = { version = "^1.0.44", features = ["derive"] } +serde_json = "1.0.44" diff --git a/src/components.rs b/src/components.rs index 2018288..94575da 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,14 +1,15 @@ use rltk::{Point, RGB}; use specs::prelude::*; +use specs::saveload::*; use specs_derive::*; -#[derive(Component)] +#[derive(Component, Clone, ConvertSaveload)] pub struct Position { pub x: i32, pub y: i32, } -#[derive(Component)] +#[derive(Component, ConvertSaveload, Clone)] pub struct Renderable { pub glyph: rltk::FontCharType, pub fg: RGB, @@ -16,28 +17,28 @@ pub struct Renderable { pub render_order: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Player {} -#[derive(Component)] +#[derive(Component, Clone, ConvertSaveload)] pub struct Viewshed { pub visible_tiles: Vec, pub range: i32, pub dirty: bool, } -#[derive(Component)] +#[derive(Component, Serialize, Deserialize, Clone)] pub struct Monster {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct Name { pub name: String, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize)] pub struct BlocksTile {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct CombatStats { pub max_hp: i32, pub hp: i32, @@ -45,12 +46,12 @@ pub struct CombatStats { pub power: i32, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, ConvertSaveload)] pub struct WantsToMelee { pub target: Entity, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct SufferDamage { pub amount: Vec, } @@ -68,60 +69,67 @@ impl SufferDamage { } } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize)] pub struct Item {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct Potion { pub heal_amount: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct InBackpack { pub owner: Entity, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, ConvertSaveload)] pub struct WantsToPickupItem { pub collected_by: Entity, pub item: Entity, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, ConvertSaveload)] pub struct WantsToUseItem { pub item: Entity, pub target: Option, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, ConvertSaveload)] pub struct WantsToDropItem { pub item: Entity, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize)] pub struct Consumable {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct ProvidesHealing { pub heal_amount: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, ConvertSaveload)] pub struct Ranged { pub range: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, ConvertSaveload)] pub struct InflictsDamage { pub damage: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct AreaOfEffect { pub radius: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct Confusion { pub turns: i32, } + +pub struct SerializeMe; + +#[derive(Component, Clone, Serialize, Deserialize)] +pub struct SerializationHelper { + pub map: super::map::Map, +} \ No newline at end of file diff --git a/src/gui.rs b/src/gui.rs index bfa1175..64574d2 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,6 +1,6 @@ use rltk::Rltk; use rltk::RGB; -use rltk::{BTerm, Point, VirtualKeyCode}; +use rltk::{Point, VirtualKeyCode}; use specs::prelude::*; use crate::gamelog::GameLog; @@ -8,7 +8,7 @@ use crate::Map; use crate::Name; use crate::Player; use crate::Position; -use crate::{CombatStats, InBackpack, State, Viewshed}; +use crate::{CombatStats, InBackpack, RunState, State, Viewshed}; pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { ctx.draw_box( @@ -388,3 +388,116 @@ pub fn ranged_target( (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 { + let runstate = gs.ecs.fetch::(); + + ctx.print_color_centered( + 15, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + "Rust Roguelike Tutorial", + ); + + if let RunState::MainMenu { + menu_selection: selection, + } = *runstate + { + if selection == MainMenuSelection::NewGame { + ctx.print_color_centered( + 24, + RGB::named(rltk::MAGENTA), + RGB::named(rltk::BLACK), + "Begin New Game", + ); + } else { + ctx.print_color_centered( + 24, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + "Begin New Game", + ); + } + + if selection == MainMenuSelection::LoadGame { + ctx.print_color_centered( + 25, + RGB::named(rltk::MAGENTA), + RGB::named(rltk::BLACK), + "Load Game", + ); + } else { + ctx.print_color_centered( + 25, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + "Load Game", + ); + } + + if selection == MainMenuSelection::Quit { + ctx.print_color_centered( + 26, + RGB::named(rltk::MAGENTA), + RGB::named(rltk::BLACK), + "Quit", + ); + } else { + ctx.print_color_centered(26, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Quit"); + } + + return match ctx.key { + None => MainMenuResult::NoSelection { + selected: selection, + }, + Some(key) => match key { + VirtualKeyCode::Escape => MainMenuResult::NoSelection { + selected: MainMenuSelection::Quit, + }, + VirtualKeyCode::Up | VirtualKeyCode::K => { + let newselection = match selection { + MainMenuSelection::NewGame => MainMenuSelection::Quit, + MainMenuSelection::LoadGame => MainMenuSelection::NewGame, + MainMenuSelection::Quit => MainMenuSelection::LoadGame, + }; + MainMenuResult::NoSelection { + selected: newselection, + } + } + VirtualKeyCode::Down | VirtualKeyCode::J => { + let newselection = match selection { + MainMenuSelection::NewGame => MainMenuSelection::LoadGame, + MainMenuSelection::LoadGame => MainMenuSelection::Quit, + MainMenuSelection::Quit => MainMenuSelection::NewGame, + }; + MainMenuResult::NoSelection { + selected: newselection, + } + } + VirtualKeyCode::Return | VirtualKeyCode::I => MainMenuResult::Selected { + selected: selection, + }, + _ => MainMenuResult::NoSelection { + selected: selection, + }, + }, + }; + } + + MainMenuResult::NoSelection { + selected: MainMenuSelection::NewGame, + } +} diff --git a/src/inventory_system.rs b/src/inventory_system.rs index 2222e83..bca9812 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,10 +1,11 @@ -use rltk::Rltk; use specs::prelude::*; -use crate::{AreaOfEffect, CombatStats, Confusion, Consumable, InBackpack, InflictsDamage, Map, Name, Position, Potion, ProvidesHealing, Ranged, State, SufferDamage, WantsToDropItem, WantsToPickupItem, WantsToUseItem}; use crate::gamelog::GameLog; -use crate::gui::ItemMenuResult; -use crate::spawner::{confusion_scroll, player}; +use crate::{ + AreaOfEffect, CombatStats, Confusion, Consumable, InBackpack, InflictsDamage, Map, Name, + Position, ProvidesHealing, Ranged, SufferDamage, WantsToDropItem, WantsToPickupItem, + WantsToUseItem, +}; pub struct ItemCollectionSystem {} @@ -80,7 +81,7 @@ impl<'a> System<'a> for ItemUseSystem { map, mut suffer_damage, aoe, - mut confused + mut confused, ) = data; for (entity, use_item) in (&entities, &wants_use).join() { @@ -88,7 +89,7 @@ impl<'a> System<'a> for ItemUseSystem { let mut targets: Vec = Vec::new(); match use_item.target { - None => { targets.push(*player_entity) } + None => targets.push(*player_entity), Some(target) => { let area_effect = aoe.get(use_item.item); match area_effect { @@ -99,8 +100,11 @@ impl<'a> System<'a> for ItemUseSystem { } } Some(area_effect) => { - let mut black_tiles = rltk::field_of_view(target, area_effect.radius, &*map); - black_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1); + let mut black_tiles = + rltk::field_of_view(target, area_effect.radius, &*map); + black_tiles.retain(|p| { + p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1 + }); for tile_idx in black_tiles.iter() { let idx = map.xy_idx(tile_idx.x, tile_idx.y); for mob in map.tile_content[idx].iter() { @@ -119,7 +123,10 @@ impl<'a> System<'a> for ItemUseSystem { if entity == *player_entity { let mob_name = names.get(*mob).unwrap(); let item_name = names.get(use_item.item).unwrap(); - game_log.entries.push(format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, item_damages.damage)); + game_log.entries.push(format!( + "You use {} on {}, inflicting {} hp.", + item_name.name, mob_name.name, item_damages.damage + )); } used_item = true; @@ -153,16 +160,20 @@ impl<'a> System<'a> for ItemUseSystem { if entity == *player_entity { let mob_name = names.get(*mob).unwrap(); let item_name = names.get(use_item.item).unwrap(); - game_log.entries.push(format!("You use {} on {}, confusing them.", item_name.name, mob_name.name)); + game_log.entries.push(format!( + "You use {} on {}, confusing them.", + item_name.name, mob_name.name + )); } } } } for mob in add_confusion.iter() { - confused.insert(mob.0, Confusion { turns: mob.1 }).expect("Unable to insert status"); + confused + .insert(mob.0, Confusion { turns: mob.1 }) + .expect("Unable to insert status"); } - if used_item { let consumable = consumables.get(use_item.item); match consumable { diff --git a/src/main.rs b/src/main.rs index 9c775ab..7beb39e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ -use std::thread::spawn; +extern crate serde; -use rltk::{GameState, Point, RGB}; +use rltk::{GameState, Point}; use specs::prelude::*; +use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; use components::*; use damage_system::DamageSystem; @@ -27,6 +28,7 @@ mod player; mod rect; mod spawner; mod visibility_system; +mod save_load_system; #[derive(PartialEq, Copy, Clone)] pub enum RunState { @@ -36,7 +38,14 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, - ShowTargeting { range: i32, item: Entity }, + ShowTargeting { + range: i32, + item: Entity, + }, + MainMenu { + menu_selection: gui::MainMenuSelection, + }, + SaveGame, } pub struct State { @@ -51,14 +60,14 @@ impl State { let mut mob = MonsterAI {}; mob.run_now(&self.ecs); - let mut mapindex = MapIndexingSystem {}; - mapindex.run_now(&self.ecs); + let mut map_index = MapIndexingSystem {}; + map_index.run_now(&self.ecs); - let mut meleesystem = MeleeCombatSystem {}; - meleesystem.run_now(&self.ecs); + let mut melee_system = MeleeCombatSystem {}; + melee_system.run_now(&self.ecs); - let mut damagesystem = DamageSystem {}; - damagesystem.run_now(&self.ecs); + let mut damage_system = DamageSystem {}; + damage_system.run_now(&self.ecs); let mut inventory = ItemCollectionSystem {}; inventory.run_now(&self.ecs); @@ -75,58 +84,61 @@ impl State { impl GameState for State { fn tick(&mut self, ctx: &mut rltk::Rltk) { + let mut new_run_state; + { + let run_state = self.ecs.fetch::(); + new_run_state = *run_state; + } + ctx.cls(); - { - draw_map(&self.ecs, ctx); + match new_run_state { + RunState::MainMenu { .. } => {} + _ => { + draw_map(&self.ecs, ctx); - let positions = self.ecs.read_storage::(); - let renderables = self.ecs.read_storage::(); - let map = self.ecs.fetch::(); + let positions = self.ecs.read_storage::(); + let renderables = self.ecs.read_storage::(); + let map = self.ecs.fetch::(); - let mut data = (&positions, &renderables).join().collect::>(); - data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); - for (pos, render) 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) + let mut data = (&positions, &renderables).join().collect::>(); + data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); + for (pos, render) 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); } - - gui::draw_ui(&self.ecs, ctx); } - let mut newrunstate; - { - let runstate = self.ecs.fetch::(); - newrunstate = *runstate; - } - - match newrunstate { + match new_run_state { RunState::PreRun => { self.run_systems(); - newrunstate = RunState::AwaitingInput; + new_run_state = RunState::AwaitingInput; } - RunState::AwaitingInput => newrunstate = player_input(self, ctx), + RunState::AwaitingInput => new_run_state = player_input(self, ctx), RunState::PlayerTurn => { self.run_systems(); - newrunstate = RunState::MonsterTurn; + new_run_state = RunState::MonsterTurn; } RunState::MonsterTurn => { self.run_systems(); - newrunstate = RunState::AwaitingInput; + new_run_state = RunState::AwaitingInput; } RunState::ShowInventory => { let result = gui::show_inventory(self, ctx); match result.0 { - gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::Cancel => new_run_state = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); let is_ranged = self.ecs.read_storage::(); let is_item_ranged = is_ranged.get(item_entity); if let Some(is_item_ranged) = is_item_ranged { - newrunstate = RunState::ShowTargeting { + new_run_state = RunState::ShowTargeting { range: is_item_ranged.range, item: item_entity, } @@ -135,10 +147,13 @@ impl GameState for State { intent .insert( *self.ecs.fetch::(), - WantsToUseItem { item: item_entity, target: None }, + WantsToUseItem { + item: item_entity, + target: None, + }, ) .expect("Unable to insert intent"); - newrunstate = RunState::PlayerTurn; + new_run_state = RunState::PlayerTurn; } } } @@ -146,7 +161,7 @@ impl GameState for State { RunState::ShowDropItem => { let result = gui::drop_item_menu(self, ctx); match result.0 { - gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::Cancel => new_run_state = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); @@ -157,27 +172,56 @@ impl GameState for State { WantsToDropItem { item: item_entity }, ) .expect("Unable to insert intent"); - newrunstate = RunState::PlayerTurn; + new_run_state = 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::Cancel => new_run_state = 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("Unable to insert intent"); - newrunstate = RunState::PlayerTurn; + intent + .insert( + *self.ecs.fetch::(), + WantsToUseItem { + item, + target: result.1, + }, + ) + .expect("Unable to insert intent"); + new_run_state = RunState::PlayerTurn; } } } + RunState::MainMenu { .. } => { + let result = gui::main_menu(self, ctx); + match result { + gui::MainMenuResult::NoSelection { selected } => { + new_run_state = RunState::MainMenu { + menu_selection: selected, + } + } + gui::MainMenuResult::Selected { selected } => match selected { + gui::MainMenuSelection::NewGame => new_run_state = RunState::PreRun, + gui::MainMenuSelection::LoadGame => new_run_state = RunState::PreRun, + gui::MainMenuSelection::Quit => ::std::process::exit(0), + }, + } + } + RunState::SaveGame => { + save_load_system::save_game(&mut self.ecs); + new_run_state = RunState::MainMenu { + menu_selection: gui::MainMenuSelection::LoadGame, + }; + } } { - let mut runwriter = self.ecs.write_resource::(); - *runwriter = newrunstate; + let mut run_writer = self.ecs.write_resource::(); + *run_writer = new_run_state; } damage_system::delete_the_dead(&mut self.ecs); } @@ -193,7 +237,6 @@ fn main() -> rltk::BError { context.with_post_scanlines(true); let mut gs = State { ecs: World::new() }; - gs.ecs.insert(RunState::PreRun); gs.ecs.insert(rltk::RandomNumberGenerator::new()); gs.ecs.insert(gamelog::GameLog { entries: vec!["Welcome to Rusty Roguelike".to_string()], @@ -220,6 +263,9 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::>(); + gs.ecs.register::(); + gs.ecs.insert(SimpleMarkerAllocator::::new()); let map = Map::new_map_rooms_and_corridors(); let (player_x, player_y) = map.rooms[0].center(); @@ -232,6 +278,9 @@ fn main() -> rltk::BError { } gs.ecs.insert(map); + gs.ecs.insert(RunState::MainMenu { + menu_selection: gui::MainMenuSelection::NewGame, + }); rltk::main_loop(context, gs) } diff --git a/src/map.rs b/src/map.rs index f8d43cd..6cdbd66 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,11 +1,12 @@ use std::cmp::{max, min}; -use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, RGB}; +use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, RGB, Rltk}; +use serde::{Deserialize, Serialize}; use specs::prelude::*; use crate::rect::Rect; -#[derive(PartialEq, Copy, Clone)] +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum TileType { Wall, Floor, @@ -17,6 +18,7 @@ pub const MAP_COUNT: usize = MAP_HEIGHT * MAP_WIDTH; pub const MAX_MONSTER: i32 = 4; pub const MAX_ITEMS: i32 = 2; +#[derive(Default, Serialize, Deserialize, Clone)] pub struct Map { pub tiles: Vec, pub rooms: Vec, @@ -25,6 +27,9 @@ pub struct Map { pub revealed_tiles: Vec, pub visible_tiles: Vec, pub blocked: Vec, + + #[serde(skip_serializing)] + #[serde(skip_deserializing)] pub tile_content: Vec>, } diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 0e689bf..283bb4a 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,8 +1,7 @@ -use crate::gamelog::GameLog; -use rltk::console; use specs::prelude::*; use crate::components::{CombatStats, Name, SufferDamage, WantsToMelee}; +use crate::gamelog::GameLog; pub struct MeleeCombatSystem {} diff --git a/src/monster_ai_system.rs b/src/monster_ai_system.rs index 5079ac4..a376659 100644 --- a/src/monster_ai_system.rs +++ b/src/monster_ai_system.rs @@ -1,7 +1,11 @@ -use rltk::{console, Point}; +use rltk::Point; use specs::prelude::*; -use crate::{components::{Monster, Position, Viewshed, WantsToMelee}, Confusion, map::Map, RunState}; +use crate::{ + components::{Monster, Position, Viewshed, WantsToMelee}, + map::Map, + Confusion, RunState, +}; pub struct MonsterAI {} @@ -39,7 +43,7 @@ impl<'a> System<'a> for MonsterAI { } for (entity, mut viewshed, _monster, mut pos) in - (&entities, &mut viewshed, &monster, &mut position).join() + (&entities, &mut viewshed, &monster, &mut position).join() { let mut can_act = true; diff --git a/src/player.rs b/src/player.rs index df6485c..94704a4 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,12 +1,12 @@ use std::cmp::{max, min}; -use rltk::{console, Point, Rltk, VirtualKeyCode}; +use rltk::{Point, Rltk, VirtualKeyCode}; use specs::prelude::*; use crate::gamelog::GameLog; use crate::{ components::{CombatStats, Player, Position, Viewshed, WantsToMelee}, - map::{Map, TileType}, + map::Map, Item, RunState, State, WantsToPickupItem, }; @@ -99,6 +99,7 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { VirtualKeyCode::G => get_item(&mut gs.ecs), VirtualKeyCode::I => return RunState::ShowInventory, VirtualKeyCode::D => return RunState::ShowDropItem, + VirtualKeyCode::Escape => return RunState::SaveGame, _ => return RunState::AwaitingInput, }, } diff --git a/src/rect.rs b/src/rect.rs index 3b74c16..83f2fee 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -1,3 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub struct Rect { pub x1: i32, pub x2: i32, diff --git a/src/save_load_system.rs b/src/save_load_system.rs new file mode 100644 index 0000000..f9b9a54 --- /dev/null +++ b/src/save_load_system.rs @@ -0,0 +1,123 @@ +use std::fs; +use std::fs::File; +use std::path::Path; + +use specs::error::NoError; +use specs::prelude::*; +use specs::saveload::{DeserializeComponents, MarkedBuilder, SerializeComponents, SimpleMarker, SimpleMarkerAllocator}; + +use super::components::*; + +macro_rules! serialize_individually { + ($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => { + $( + SerializeComponents::>::serialize( + &( $ecs.read_storage::<$type>(), ), + &$data.0, + &$data.1, + &mut $ser, + ) + .unwrap(); + )* + }; +} + +#[cfg(target_arch = "wasm32")] +pub fn save_game(_ecs: &mut World) {} + +#[cfg(not(target_arch = "wasm32"))] +pub fn save_game(ecs: &mut World) { + // Create helper + let mapcopy = ecs.get_mut::().unwrap().clone(); + let savehelper = ecs + .create_entity() + .with(SerializationHelper { map: mapcopy }) + .marked::>() + .build(); + + // Actually serialize + { + let data = (ecs.entities(), ecs.read_storage::>()); + + let writer = File::create("./savegame.json").unwrap(); + let mut serializer = serde_json::Serializer::new(writer); + serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster, + Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, + AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, + WantsToDropItem, SerializationHelper + ); + } + + // Clean up + ecs.delete_entity(savehelper).expect("Crash on cleanup"); +} + +pub fn does_save_exist() -> bool { + Path::new("./savegame.json").exists() +} + +macro_rules! deserialize_individually { + ($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => { + $( + DeserializeComponents::::deserialize( + &mut ( &mut $ecs.write_storage::<$type>(), ), + &$data.0, // entities + &mut $data.1, // marker + &mut $data.2, // allocater + &mut $de, + ) + .unwrap(); + )* + }; +} + +pub fn load_game(ecs: &mut World) { + { + // Delete everything + let mut to_delete = Vec::new(); + for e in ecs.entities().join() { + to_delete.push(e); + } + for del in to_delete.iter() { + ecs.delete_entity(*del).expect("Deletion failed"); + } + } + + let data = fs::read_to_string("./savegame.json").unwrap(); + let mut de = serde_json::Deserializer::from_str(&data); + + { + let mut d = (&mut ecs.entities(), &mut ecs.write_storage::>(), &mut ecs.write_resource::>()); + + deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, + Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, + AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, + WantsToDropItem, SerializationHelper + ); + } + + let mut deleteme: Option = None; + { + let entities = ecs.entities(); + let helper = ecs.read_storage::(); + let player = ecs.read_storage::(); + let position = ecs.read_storage::(); + for (e, h) in (&entities, &helper).join() { + let mut worldmap = ecs.write_resource::(); + *worldmap = h.map.clone(); + worldmap.tile_content = vec![Vec::new(); super::map::MAP_COUNT]; + deleteme = Some(e); + } + for (e, _p, pos) in (&entities, &player, &position).join() { + let mut ppos = ecs.write_resource::(); + *ppos = rltk::Point::new(pos.x, pos.y); + let mut player_resource = ecs.write_resource::(); + *player_resource = e; + } + } + ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper"); +} + +pub fn delete_save() { + if Path::new("./savegame.json").exists() { std::fs::remove_file("./savegame.json").expect("Unable to delete file"); } +} diff --git a/src/spawner.rs b/src/spawner.rs index ce368bc..222bace 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,7 +1,8 @@ use rltk::{FontCharType, RandomNumberGenerator, RGB}; use specs::prelude::*; +use specs::saveload::{MarkedBuilder, SimpleMarker}; -use crate::{AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, InflictsDamage, Item, MAP_WIDTH, MAX_ITEMS, MAX_MONSTER, Monster, Name, Player, Position, Potion, ProvidesHealing, Ranged, Renderable, Viewshed}; +use crate::{AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, InflictsDamage, Item, MAP_WIDTH, MAX_ITEMS, MAX_MONSTER, Monster, Name, Player, Position, ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed}; use crate::rect::Rect; pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { @@ -31,6 +32,7 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { defense: 2, power: 5, }) + .marked::>() .build() } @@ -79,6 +81,7 @@ fn monster(ecs: &mut World, x: i32, y: i32, glyph: FontCharType, na defense: 1, power: 4, }) + .marked::>() .build(); } @@ -146,6 +149,7 @@ pub fn health_potion(ecs: &mut World, x: i32, y: i32) { .with(Item {}) .with(Consumable {}) .with(ProvidesHealing { heal_amount: 8 }) + .marked::>() .build(); } @@ -165,6 +169,7 @@ pub fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Ranged { range: 6 }) .with(InflictsDamage { damage: 8 }) + .marked::>() .build(); } @@ -185,6 +190,7 @@ pub fn fireball_scroll(ecs: &mut World, x: i32, y: i32) { .with(Ranged { range: 6 }) .with(InflictsDamage { damage: 20 }) .with(AreaOfEffect { radius: 3 }) + .marked::>() .build(); } @@ -204,10 +210,10 @@ pub fn confusion_scroll(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Ranged { range: 6 }) .with(Confusion { turns: 4 }) + .marked::>() .build(); } - pub fn random_item(ecs: &mut World, x: i32, y: i32) { let roll: i32; {