Browse Source

Up to "Delving Deeper". Diverging before cont.

Simon Watson 1 year ago
parent
commit
145cd71601
12 changed files with 400 additions and 62 deletions
  1. 5 3
      Cargo.toml
  2. 5 0
      notes.md
  3. 31 20
      src/components.rs
  4. 84 2
      src/gui.rs
  5. 50 16
      src/inventory_system.rs
  6. 61 17
      src/main.rs
  7. 5 1
      src/map.rs
  8. 1 1
      src/melee_combat_system.rs
  9. 3 0
      src/player.rs
  10. 3 0
      src/rect.rs
  11. 124 0
      src/saveload_system.rs
  12. 28 2
      src/spawner.rs

+ 5 - 3
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.16.1"
-specs-derive = "0.4.1"
+rltk = { version = "0.8.0", features = ["serde"] }
+specs = { version = "0.17", features = ["serde"] }
+specs-derive = "0.4.1"
+serde = { version = "^1.0.44", features = ["derive"] }
+serde_json = "^1.0.44"

+ 5 - 0
notes.md

@@ -17,3 +17,8 @@ that relates to that.
 * * Sacrifice turns while in combat to gain faith (Pacifism)
 * * Convert HP to Faith (mana) (Flagilation) OR
 * * * No HP/Mana, shared resource pool that is used for HP and casting (Faith)
+
+# Reminders
+* When adding components, from the tutorial:
+Adding components has gained some steps: we have to register them in main, tag them for Serialize, Deserialize, and remember to add them to our component type lists in saveload_system.rs. That could be easier - but it's a very solid foundation.
+

+ 31 - 20
src/components.rs

@@ -1,57 +1,68 @@
 use specs::prelude::*;
 use specs_derive::*;
 use rltk::{RGB};
+use serde::{Serialize, Deserialize};
+use specs::saveload::{Marker, ConvertSaveload};
+use specs::error::NoError;
 
 // COMPONENTS
-#[derive(Component, Debug)]
+// Special component that exists to help serialize the game data
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct SerializationHelper {
+    pub map : super::map::Map
+}
+
+pub struct SerializeMe;
+
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct AreaOfEffect {
     pub radius : i32
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct Ranged {
     pub range : i32
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct InflictsDamage {
     pub damage : i32
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
 pub struct Consumable {}
 
-#[derive(Component, Debug, Clone)]
+#[derive(Component, Debug, ConvertSaveload)]
 pub struct WantsToDropItem {
     pub item : Entity
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload)]
 pub struct WantsToUseItem {
     pub item : Entity,
     pub target: Option<rltk::Point>
 }
 
-#[derive(Component, Debug, Clone)]
+#[derive(Component, Debug, ConvertSaveload)]
 pub struct WantsToPickupItem {
     pub collected_by : Entity,
     pub item : Entity
 }
 
-#[derive(Component, Debug, Clone)]
+#[derive(Component, Debug, ConvertSaveload)]
 pub struct InBackpack {
     pub owner : Entity
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
 pub struct Item {}
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct ProvidesHealing {
     pub heal_amount : i32
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct SufferDamage {
     pub amount : Vec<i32>
 }
@@ -66,12 +77,12 @@ impl SufferDamage {
     }
 }
 
-#[derive(Component, Debug, Clone)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct WantsToMelee {
     pub target : Entity
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct CombatStats {
     pub max_hp : i32,
     pub hp : i32,
@@ -79,31 +90,31 @@ pub struct CombatStats {
     pub power : i32
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
 pub struct BlocksTile {}
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, ConvertSaveload, Clone)]
 pub struct Name {
     pub name : String
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Debug, Serialize, Deserialize, Clone)]
 pub struct Monster {}
 
-#[derive(Component)]
+#[derive(Component, ConvertSaveload, Clone)]
 pub struct Viewshed {
     pub visible_tiles : Vec<rltk::Point>,
     pub range : i32,
     pub dirty: bool
 }
 
-#[derive(Component)]
+#[derive(Component, ConvertSaveload, Clone)]
 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,
@@ -111,5 +122,5 @@ pub struct Renderable {
     pub render_order : i32
 }
 
-#[derive(Component, Debug)]
+#[derive(Component, Serialize, Deserialize, Clone)]
 pub struct Player {}

+ 84 - 2
src/gui.rs

@@ -3,10 +3,92 @@ use specs::prelude::*;
 use super::{CombatStats, Player, GameLog, 
             MAPHEIGHT, Map, Name, 
             Position, Point, InBackpack,
-            State, Viewshed};
+            State, Viewshed, RunState};
 
 const GUI_HEIGHT: usize = 50 - MAPHEIGHT - 1;
 
+#[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 save_exists = super::saveload_system::does_save_exist();
+    let runstate = gs.ecs.fetch::<RunState>();
+
+    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 save_exists {
+            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");
+        }
+
+        match ctx.key {
+            None => return MainMenuResult::NoSelection{ selected: selection },
+            Some(key) => {
+                match key {
+                    VirtualKeyCode::Escape => { return MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } }
+                    VirtualKeyCode::Up => {
+                        let mut newselection;
+                        match selection {
+                            MainMenuSelection::NewGame => newselection = MainMenuSelection::Quit,
+                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::NewGame,
+                            MainMenuSelection::Quit => newselection = MainMenuSelection::LoadGame
+                        }
+                        if newselection == MainMenuSelection::LoadGame && !save_exists {
+                            newselection = MainMenuSelection::NewGame;
+                        }
+                        return MainMenuResult::NoSelection{ selected: newselection }
+                    }
+                    VirtualKeyCode::Down => {
+                        let mut newselection;
+                        match selection {
+                            MainMenuSelection::NewGame => newselection = MainMenuSelection::LoadGame,
+                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::Quit,
+                            MainMenuSelection::Quit => newselection = MainMenuSelection::NewGame
+                        }
+                        if newselection == MainMenuSelection::LoadGame && !save_exists {
+                            newselection = MainMenuSelection::Quit;
+                        }
+                        return MainMenuResult::NoSelection{ selected: newselection }
+                    }
+                    VirtualKeyCode::Return => return MainMenuResult::Selected{ selected : selection },
+                    _ => return MainMenuResult::NoSelection{ selected: selection }
+                }
+            }
+        }
+    }
+
+    MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }
+}
 pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenuResult, Option<Point>) {
     let player_entity = gs.ecs.fetch::<Entity>();
     let player_pos = gs.ecs.fetch::<Point>();
@@ -55,7 +137,7 @@ pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
     let combat_stats = ecs.read_storage::<CombatStats>();
     let players = ecs.read_storage::<Player>();
     for (_player, stats) in (&players, &combat_stats).join() {
-        let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp);
+        let health = format!("Faith: {} / {} ", stats.hp, stats.max_hp);
         ctx.print_color(12, 38, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health);
 
         ctx.draw_bar_horizontal(28, 38, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK));

+ 50 - 16
src/inventory_system.rs

@@ -4,7 +4,7 @@ use crate::{Consumable, ProvidesHealing, InflictsDamage};
 use super::{WantsToPickupItem, Name, InBackpack, 
             Position, GameLog, WantsToUseItem,
             CombatStats, Item, WantsToDropItem,
-            Map, SufferDamage};
+            Map, SufferDamage, AreaOfEffect};
 
 pub struct ItemCollectionSystem {}
 
@@ -49,7 +49,9 @@ impl<'a> System<'a> for ItemUseSystem {
                         ReadStorage<'a, Consumable>,
                         ReadStorage<'a, Item>,
                         ReadStorage<'a, InflictsDamage>,
-                        WriteStorage<'a, SufferDamage>
+                        WriteStorage<'a, SufferDamage>,
+                        ReadStorage<'a, AreaOfEffect>,
+
                       );
 
     fn run(&mut self, data : Self::SystemData) {
@@ -62,21 +64,48 @@ impl<'a> System<'a> for ItemUseSystem {
             healing,
             mut combat_stats,
             consumables,
-            item,
+            _item,
             inflict_damage,
-            mut suffer_damage) = data;
-
-        for (entity, useitem, stats) in (&entities, &wants_use, &mut combat_stats).join() {
-            let mut used_item = true;
+            mut suffer_damage,
+            aoe) = data;
+
+        for (entity, useitem) in (&entities, &wants_use).join() {
+
+            // Targeting
+            let mut targets : Vec<Entity> = Vec::new();
+            match useitem.target {
+                None => { targets.push( *player_entity ); }
+                Some(target) => {
+                    let area_effect = aoe.get(useitem.item);
+                    match area_effect {
+                        None => {
+                            // Single target in tile
+                            let idx = map.xy_idx(target.x, target.y);
+                            for mob in map.tile_content[idx].iter() {
+                                targets.push(*mob);
+                            }
+                        }
+                        Some(area_effect) => {
+                            // AoE
+                            let mut blast_tiles = rltk::field_of_view(target, area_effect.radius, &*map);
+                            blast_tiles.retain(|p| p.x > 0 && p.x < map.width-1 && p.y > 0 && p.y < map.height-1 );
+                            for tile_idx in blast_tiles.iter() {
+                                let idx = map.xy_idx(tile_idx.x, tile_idx.y);
+                                for mob in map.tile_content[idx].iter() {
+                                    targets.push(*mob);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            
             // If it inflicts damage, apply it to the target cell
             let item_damages = inflict_damage.get(useitem.item);
             match item_damages {
                 None => {}
                 Some(damage) => {
-                    let target_point = useitem.target.unwrap();
-                    let idx = map.xy_idx(target_point.x, target_point.y);
-                    used_item = false;
-                    for mob in map.tile_content[idx].iter() {
+                    for mob in targets.iter() {
                         SufferDamage::new_damage(&mut suffer_damage, *mob, damage.damage);
                         if entity == *player_entity {
                             let mob_name = names.get(*mob).unwrap();
@@ -84,22 +113,27 @@ impl<'a> System<'a> for ItemUseSystem {
                             gamelog.entries.push(format!("You use {} on {}, inflicting {} damage.", item_name.name, mob_name.name, damage.damage));
                         }
 
-                        used_item = true;
                     }
                 }
             }
             
+            // If it heals, apply the healing
             let item_heals = healing.get(useitem.item);
             match item_heals {
                 None => {}
                 Some(healer) => {
-                    stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount);
-                    if entity == *player_entity {
-                        gamelog.entries.push(format!("You drink the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
+                    for target in targets.iter() {
+                        let stats = combat_stats.get_mut(*target);
+                        if let Some(stats) = stats {
+                            stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount);
+                            if entity == *player_entity {
+                                gamelog.entries.push(format!("You use the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount));
+                            }
+                        }
                     }
                 }
             }
-
+            
             let consumable = consumables.get(useitem.item);
             match consumable {
             None => {}

+ 61 - 17
src/main.rs

@@ -1,5 +1,8 @@
 use rltk::{GameState, Rltk, Point};
 use specs::prelude::*;
+use specs::saveload::{SimpleMarker, SimpleMarkerAllocator};
+
+extern crate serde;
 
 mod components;
 pub use components::*;
@@ -25,6 +28,7 @@ use gamelog::GameLog;
 mod spawner;
 mod inventory_system;
 use inventory_system::*;
+mod saveload_system;
 
 // ***** //
 // STATE //
@@ -39,7 +43,12 @@ pub enum RunState {
     ShowDropItem,
     ShowTargeting { 
         range : i32, 
-        item : Entity },
+        item : Entity 
+    },
+    MainMenu { 
+        menu_selection: gui::MainMenuSelection
+    },
+    SaveGame,
 }
 
 pub struct State {
@@ -70,13 +79,36 @@ impl State {
 
 impl GameState for State {
     fn tick(&mut self, ctx : &mut Rltk) {
-        ctx.cls();
         let mut newrunstate;
         {
             let runstate = self.ecs.fetch::<RunState>();
             newrunstate = *runstate;
         }
 
+        ctx.cls();
+
+        match newrunstate {
+            RunState::MainMenu{..} => {}
+            _ => {
+                draw_map(&self.ecs, ctx);
+
+                {
+                    let positions = self.ecs.read_storage::<Position>();
+                    let renderables = self.ecs.read_storage::<Renderable>();
+                    let map = self.ecs.fetch::<Map>();
+
+                    let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
+                    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);
+                }
+            }
+        }
+
         match newrunstate {
             RunState::PreRun => {
                 self.run_systems();
@@ -140,28 +172,36 @@ impl GameState for State {
                     }
                 }
             }
+            RunState::MainMenu{ .. } => {
+                let result = gui::main_menu(self, ctx);
+                match result {
+                    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::SaveGame => {
+                saveload_system::save_game(&mut self.ecs);
+                newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame };
+            }
         }
 
         {
             let mut runwriter = self.ecs.write_resource::<RunState>();
             *runwriter = newrunstate;
         }
+        
         damage_system::delete_the_dead(&mut self.ecs);
 
-        draw_map(&self.ecs, ctx);
-
-        let positions = self.ecs.read_storage::<Position>();
-        let renderables = self.ecs.read_storage::<Renderable>();
-        let map = self.ecs.fetch::<Map>();
-
-        let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
-        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);
     }
 }
 
@@ -199,7 +239,11 @@ fn main() -> rltk::BError {
     gs.ecs.register::<Consumable>();
     gs.ecs.register::<Ranged>();
     gs.ecs.register::<InflictsDamage>();
+    gs.ecs.register::<AreaOfEffect>();
+    gs.ecs.register::<SimpleMarker<SerializeMe>>();
+    gs.ecs.register::<SerializationHelper>();
 
+    gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());
 
     let map = Map::new_map_rooms_and_corridors();
     let (player_x, player_y) = map.rooms[0].center();
@@ -217,7 +261,7 @@ fn main() -> rltk::BError {
 
     gs.ecs.insert(map);
     gs.ecs.insert(Point::new(player_x, player_y));
-    gs.ecs.insert(RunState::PreRun);
+    gs.ecs.insert(RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame });
 
     gs.ecs.insert(gamelog::GameLog{ entries : vec!["Saint Antony casts out...".to_string()] });
 

+ 5 - 1
src/map.rs

@@ -2,17 +2,19 @@ use rltk::{RandomNumberGenerator, RGB, Rltk, Algorithm2D, Point, BaseMap};
 use super::{Rect};
 use std::cmp::{max, min};
 use specs::prelude::*;
+use serde::{Serialize, Deserialize};
 
 pub const MAPWIDTH : usize = 80;
 pub const MAPHEIGHT : usize = 38;
 pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
 
-#[derive(PartialEq, Copy, Clone)]
+#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
 pub enum TileType {
     Wall, 
     Floor
 }
 
+#[derive(Default, Serialize, Deserialize, Clone)]
 pub struct Map {
     pub tiles: Vec<TileType>,
     pub rooms: Vec<Rect>,
@@ -21,6 +23,8 @@ pub struct Map {
     pub revealed_tiles : Vec<bool>,
     pub visible_tiles: Vec<bool>,
     pub blocked : Vec<bool>,
+    #[serde(skip_serializing)]
+    #[serde(skip_deserializing)]
     pub tile_content : Vec<Vec<Entity>>
 }
 impl Map {

+ 1 - 1
src/melee_combat_system.rs

@@ -26,7 +26,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
                     if damage == 0 {
                         log.entries.push(format!("{} is unable to hurt {}", &name.name, &target_name.name));
                     } else {
-                        log.entries.push(format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage));
+                        log.entries.push(format!("{} hits {}, for {} damage.", &name.name, &target_name.name, damage));
                         SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage);
                     }
                 }

+ 3 - 0
src/player.rs

@@ -109,6 +109,9 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
             VirtualKeyCode::G => get_item(&mut gs.ecs),
             VirtualKeyCode::I => return RunState::ShowInventory,
             VirtualKeyCode::X => return RunState::ShowDropItem,
+
+            // Save and Quit
+            VirtualKeyCode::Escape => return RunState::SaveGame,
             
             _ => { return RunState::AwaitingInput }
         },

+ 3 - 0
src/rect.rs

@@ -1,3 +1,6 @@
+use serde::{Serialize, Deserialize};
+
+#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
 pub struct Rect {
     pub x1 : i32,
     pub x2 : i32,

+ 124 - 0
src/saveload_system.rs

@@ -0,0 +1,124 @@
+use specs::prelude::*;
+use specs::saveload::{SimpleMarker, SimpleMarkerAllocator, SerializeComponents, DeserializeComponents, MarkedBuilder};
+use std::convert::Infallible;
+use super::components::*;
+use std::fs::File;
+use std::path::Path;
+use std::fs;
+
+macro_rules! serialize_individually {
+    ($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => {
+        $(
+        SerializeComponents::<Infallible, SimpleMarker<SerializeMe>>::serialize(
+            &( $ecs.read_storage::<$type>(), ),
+            &$data.0,
+            &$data.1,
+            &mut $ser,
+        )
+        .unwrap();
+        )*
+    };
+}
+
+macro_rules! deserialize_individually {
+    ($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
+        $(
+        DeserializeComponents::<Infallible, _>::deserialize(
+            &mut ( &mut $ecs.write_storage::<$type>(), ),
+            &mut $data.0, // entities
+            &mut $data.1, // marker
+            &mut $data.2, // allocater
+            &mut $de,
+        )
+        .unwrap();
+        )*
+    };
+}
+
+pub fn does_save_exist() -> bool {
+    Path::new("./savegame.json").exists()
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn save_game(ecs : &mut World) {
+    // Create helper
+    let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone();
+    let savehelper = ecs
+        .create_entity()
+        .with(SerializationHelper{ map : mapcopy })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+
+    // Actually serialize
+    {
+        let data = ( ecs.entities(), ecs.read_storage::<SimpleMarker<SerializeMe>>() );
+
+        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, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
+            WantsToDropItem, SerializationHelper
+        );
+    }
+
+    // Clean up
+    ecs.delete_entity(savehelper).expect("Crash on cleanup");
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn save_game(_ecs : &mut World) {
+}
+
+pub fn delete_save() {
+    if Path::new("./savegame.json").exists() {
+        std::fs::remove_file("./savegame.json").expect("Unable to delete file");
+    } 
+}
+
+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::<SimpleMarker<SerializeMe>>(), &mut ecs.write_resource::<SimpleMarkerAllocator<SerializeMe>>());
+
+        deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, 
+            Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
+            AreaOfEffect, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
+            WantsToDropItem, SerializationHelper
+        );
+    }
+
+    let mut deleteme : Option<Entity> = None;
+    {
+        let entities = ecs.entities();
+        let helper = ecs.read_storage::<SerializationHelper>();
+        let player = ecs.read_storage::<Player>();
+        let position = ecs.read_storage::<Position>();
+        for (e,h) in (&entities, &helper).join() {
+            let mut worldmap = ecs.write_resource::<super::map::Map>();
+            *worldmap = h.map.clone();
+            worldmap.tile_content = vec![Vec::new(); super::map::MAPCOUNT];
+            deleteme = Some(e);
+        }
+        for (e,_p,pos) in (&entities, &player, &position).join() {
+            let mut ppos = ecs.write_resource::<rltk::Point>();
+            *ppos = rltk::Point::new(pos.x, pos.y);
+            let mut player_resource = ecs.write_resource::<Entity>();
+            *player_resource = e;
+        }
+    }
+    ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper");
+}

+ 28 - 2
src/spawner.rs

@@ -4,7 +4,9 @@ use super::{CombatStats, Player, Renderable,
             Name, Position, Viewshed, 
             Monster, BlocksTile, Rect, 
             MAPWIDTH, Item, ProvidesHealing,
-            Consumable, InflictsDamage, Ranged};
+            Consumable, InflictsDamage, Ranged,
+            AreaOfEffect,SerializeMe};
+use specs::saveload::{MarkedBuilder, SimpleMarker};
 
 const MAX_MONSTERS : i32 = 4;
 const MAX_ITEMS : i32 = 2; // PER MAP
@@ -13,14 +15,34 @@ fn random_item(ecs: &mut World, x: i32, y: i32) {
     let roll :i32;
     {
         let mut rng = ecs.write_resource::<RandomNumberGenerator>();
-        roll = rng.roll_dice(1, 2);
+        roll = rng.roll_dice(1, 3);
     }
     match roll {
         1 => { health_potion(ecs, x, y) }
+        2 => { fireball_scroll(ecs, x, y) }
         _ => { magic_missile_scroll(ecs, x, y) }
     }
 }
 
+fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {
+    ecs.create_entity()
+        .with(Position{ x, y })
+        .with(Renderable{
+            glyph: rltk::to_cp437(')'),
+            fg: RGB::named(rltk::ORANGE),
+            bg: RGB::named(rltk::BLACK),
+            render_order: 2
+        })
+        .with(Name{ name : "Fireball Scroll".to_string() })
+        .with(Item{})
+        .with(Consumable{})
+        .with(Ranged{ range: 6 })
+        .with(InflictsDamage{ damage: 20 })
+        .with(AreaOfEffect{ radius: 3 })
+        .marked::<SimpleMarker<SerializeMe>>()
+        .build();
+}
+
 fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {
     ecs.create_entity()
         .with(Position{ x, y })
@@ -35,6 +57,7 @@ fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) {
         .with(Consumable{})
         .with(Ranged{ range: 6 })
         .with(InflictsDamage{ damage: 8 })
+        .marked::<SimpleMarker<SerializeMe>>()
         .build();
 }
 
@@ -51,6 +74,7 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) {
         .with(Item{})
         .with(Consumable{})
         .with(ProvidesHealing{ heal_amount: 8 })
+        .marked::<SimpleMarker<SerializeMe>>()
         .build();
 }
 
@@ -123,6 +147,7 @@ pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
         .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
         .with(Name{name: "Player".to_string() })
         .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
+        .marked::<SimpleMarker<SerializeMe>>()
         .build()
 }
 
@@ -156,5 +181,6 @@ fn monster<S : ToString>(ecs: &mut World, x: i32, y: i32, glyph : rltk::FontChar
         .with(Name{ name : name.to_string() })
         .with(BlocksTile{})
         .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
+        .marked::<SimpleMarker<SerializeMe>>()
         .build();
 }