diff --git a/.gitignore b/.gitignore index d5f5603..f9598fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .rustfmt.toml -.vscode/* \ No newline at end of file +.vscode/* +savegame.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2930b82..817d22f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,7 @@ dependencies = [ "byteorder", "lazy_static", "parking_lot 0.11.2", + "serde", ] [[package]] @@ -169,6 +170,7 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f31b525fcd65027885f3a1e3a250a5dd397d70de4a6a5a125f03e0bef951499" dependencies = [ + "serde", "ultraviolet 0.9.1", ] @@ -262,6 +264,7 @@ dependencies = [ "rand", "rand_xorshift", "regex", + "serde", "wasm-bindgen", ] @@ -2275,6 +2278,8 @@ dependencies = [ "bracket-lib 0.8.7 (git+https://github.com/amethyst/bracket-lib.git?rev=851f6f08675444fb6fa088b9e67bee9fd75554c6)", "criterion", "rltk", + "serde", + "serde_json", "specs", "specs-derive", ] @@ -2545,6 +2550,7 @@ dependencies = [ "hibitset", "log", "rayon", + "serde", "shred", "shrev", "tuple_utils", diff --git a/Cargo.toml b/Cargo.toml index bb09bd4..33a049c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rltk = { version = "^0.8.7" } +rltk = { version = "^0.8.7", features = ["serde"] } bracket-lib = { git = "https://github.com/amethyst/bracket-lib.git", rev = "851f6f08675444fb6fa088b9e67bee9fd75554c6", features = ["serde"] } -specs = "0.16.1" +specs = { version = "0.16.1", features = ["serde"] } specs-derive = "0.4.1" +serde = { version = "1.0.93", features = ["derive"]} +serde_json = "1.0.39" [dev-dependencies] criterion = { version = "^0.5" } diff --git a/src/components.rs b/src/components.rs index 972cc8e..5d6e2b1 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,14 +1,26 @@ use rltk::RGB; +use serde::{Deserialize, Serialize}; +use specs::error::NoError; use specs::prelude::*; +use specs::saveload::{ConvertSaveload, Marker}; use specs_derive::*; -#[derive(Component)] +// Serialization helper code. We need to implement ConvertSaveload for each type that contains an +// 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, +} + +#[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, @@ -16,28 +28,28 @@ pub struct Renderable { pub render_order: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Player {} -#[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, pub range: i32, pub dirty: bool, } -#[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 BlocksTile {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct CombatStats { pub max_hp: i32, pub hp: i32, @@ -45,12 +57,12 @@ pub struct CombatStats { pub power: i32, } -#[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 SufferDamage { pub amount: Vec, } @@ -66,63 +78,63 @@ impl SufferDamage { } } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Item {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct ProvidesHealing { pub amount: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct InflictsDamage { pub amount: 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 AOE { pub radius: i32, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct Confusion { pub turns: i32, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, ConvertSaveload)] pub struct InBackpack { pub owner: Entity, } -#[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 WantsToDropItem { pub item: Entity, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct WantsToUseItem { pub item: Entity, pub target: Option, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Consumable {} -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Destructible {} -#[derive(Component, Clone)] +#[derive(Component, Clone, ConvertSaveload)] pub struct ParticleLifetime { pub lifetime_ms: f32, } diff --git a/src/gui.rs b/src/gui.rs index 4658f7e..2c5f1f8 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -273,34 +273,40 @@ pub enum MainMenuResult { } 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::(); let assets = gs.ecs.fetch::(); ctx.render_xp_sprite(&assets.menu, 0, 0); - ctx.print_color(38, 21, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "RUST-RL"); + ctx.print_color(40, 21, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "RUST-RL"); if let RunState::MainMenu { menu_selection: selection } = *runstate { + let mut y = 24; if selection == MainMenuSelection::NewGame { - ctx.print_color(34, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); - ctx.print_color(36, 24, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "new game"); - ctx.print_color(45, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); + ctx.print_color(37, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); + ctx.print_color(39, 24, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "new game"); + ctx.print_color(48, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); } else { - ctx.print_color(36, 24, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "new game"); + ctx.print_color(39, 24, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "new game"); } - if selection == MainMenuSelection::LoadGame { - ctx.print_color(38, 26, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); - ctx.print_color(40, 26, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "load game"); - ctx.print_color(50, 26, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); - } else { - ctx.print_color(40, 26, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "load game"); + y += 2; + if save_exists { + if selection == MainMenuSelection::LoadGame { + ctx.print_color(36, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); + ctx.print_color(38, y, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "load game"); + ctx.print_color(48, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); + } else { + ctx.print_color(38, y, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "load game"); + } + y += 2; } if selection == MainMenuSelection::Quit { - ctx.print_color(34, 28, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); - ctx.print_color(36, 28, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "goodbye!"); - ctx.print_color(45, 28, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); + ctx.print_color(37, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); + ctx.print_color(39, y, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "goodbye!"); + ctx.print_color(48, y, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); } else { - ctx.print_color(36, 28, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "quit"); + ctx.print_color(43, y, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "quit"); } match ctx.key { @@ -312,21 +318,27 @@ pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult { VirtualKeyCode::N => return MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }, VirtualKeyCode::L => return MainMenuResult::NoSelection { selected: MainMenuSelection::LoadGame }, VirtualKeyCode::Up => { - let new_selection; + let mut new_selection; match selection { MainMenuSelection::NewGame => new_selection = MainMenuSelection::Quit, MainMenuSelection::LoadGame => new_selection = MainMenuSelection::NewGame, MainMenuSelection::Quit => new_selection = MainMenuSelection::LoadGame, } + if new_selection == MainMenuSelection::LoadGame && !save_exists { + new_selection = MainMenuSelection::NewGame; + } return MainMenuResult::NoSelection { selected: new_selection }; } VirtualKeyCode::Down => { - let new_selection; + let mut new_selection; match selection { MainMenuSelection::NewGame => new_selection = MainMenuSelection::LoadGame, MainMenuSelection::LoadGame => new_selection = MainMenuSelection::Quit, MainMenuSelection::Quit => new_selection = MainMenuSelection::NewGame, } + if new_selection == MainMenuSelection::LoadGame && !save_exists { + new_selection = MainMenuSelection::Quit; + } return MainMenuResult::NoSelection { selected: new_selection }; } VirtualKeyCode::Return => return MainMenuResult::Selected { selected: selection }, diff --git a/src/main.rs b/src/main.rs index 0b506fd..a4db22f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ use rltk::{GameState, Point, Rltk, RGB}; use specs::prelude::*; +use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; use std::ops::Add; +extern crate serde; mod components; pub use components::*; @@ -12,6 +14,7 @@ mod rect; pub use rect::Rect; mod gamelog; mod gui; +mod saveload_system; mod spawner; mod visibility_system; use visibility_system::VisibilitySystem; @@ -44,6 +47,7 @@ pub enum RunState { ShowDropItem, ShowTargeting { range: i32, item: Entity, aoe: i32 }, MainMenu { menu_selection: gui::MainMenuSelection }, + SaveGame, } pub struct State { @@ -200,22 +204,23 @@ impl GameState for State { gui::MainMenuResult::NoSelection { selected } => { new_runstate = RunState::MainMenu { menu_selection: selected } } - gui::MainMenuResult::Selected { selected } => { - match selected { - gui::MainMenuSelection::NewGame => new_runstate = RunState::PreRun, - gui::MainMenuSelection::LoadGame => { - new_runstate = RunState::PreRun; - //saveload_system::load_game(&mut self.ecs); - //rew_runstate = RunState::AwaitingInput; - //saveload_system::delete_save(); - } - gui::MainMenuSelection::Quit => { - ::std::process::exit(0); - } + gui::MainMenuResult::Selected { selected } => match selected { + gui::MainMenuSelection::NewGame => new_runstate = RunState::PreRun, + gui::MainMenuSelection::LoadGame => { + saveload_system::load_game(&mut self.ecs); + new_runstate = RunState::AwaitingInput; + saveload_system::delete_save(); } - } + gui::MainMenuSelection::Quit => { + ::std::process::exit(0); + } + }, } } + RunState::SaveGame => { + saveload_system::save_game(&mut self.ecs); + new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame }; + } } { @@ -268,6 +273,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].centre(); diff --git a/src/map.rs b/src/map.rs index eaa37c6..679bf1d 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,11 +1,12 @@ use super::Rect; use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, RGB}; +use serde::{Deserialize, Serialize}; use specs::prelude::*; use std::cmp::{max, min}; use std::collections::HashSet; use std::ops::{Add, Mul}; -#[derive(PartialEq, Copy, Clone)] +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum TileType { Wall, Floor, @@ -14,9 +15,9 @@ pub enum TileType { pub const MAPWIDTH: usize = 80; pub const MAPHEIGHT: usize = 43; const MAX_OFFSET: u8 = 32; -const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH; +pub const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH; -#[derive(Default)] +#[derive(Default, Serialize, Deserialize, Clone)] pub struct Map { pub tiles: Vec, pub rooms: Vec, @@ -28,8 +29,11 @@ pub struct Map { pub green_offset: Vec, pub blue_offset: Vec, pub blocked: Vec, - pub tile_content: Vec>, pub bloodstains: HashSet, + + #[serde(skip_serializing)] + #[serde(skip_deserializing)] + pub tile_content: Vec>, } impl Map { @@ -98,8 +102,8 @@ impl Map { green_offset: vec![0; MAPCOUNT], blue_offset: vec![0; MAPCOUNT], blocked: vec![false; MAPCOUNT], - tile_content: vec![Vec::new(); MAPCOUNT], bloodstains: HashSet::new(), + tile_content: vec![Vec::new(); MAPCOUNT], }; const MAX_ROOMS: i32 = 30; diff --git a/src/player.rs b/src/player.rs index bc6a634..e8e7d90 100644 --- a/src/player.rs +++ b/src/player.rs @@ -98,7 +98,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::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }, + VirtualKeyCode::Escape => return RunState::SaveGame, _ => { return RunState::AwaitingInput; } diff --git a/src/rect.rs b/src/rect.rs index 6109287..b705387 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/saveload_system.rs b/src/saveload_system.rs new file mode 100644 index 0000000..b6ddee8 --- /dev/null +++ b/src/saveload_system.rs @@ -0,0 +1,170 @@ +use super::components::*; +use specs::error::NoError; +use specs::prelude::*; +use specs::saveload::{DeserializeComponents, MarkedBuilder, SerializeComponents, SimpleMarker, SimpleMarkerAllocator}; +use std::fs; +use std::fs::File; +use std::path::Path; + +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, + Destructible, + Ranged, + InflictsDamage, + AOE, + 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, + Destructible, + Ranged, + InflictsDamage, + AOE, + 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::MAPCOUNT]; + 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 bbd6d9d..a16ad4b 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,9 +1,10 @@ use super::{ BlocksTile, CombatStats, Confusion, Consumable, Destructible, InflictsDamage, Item, Monster, Name, Player, - Position, ProvidesHealing, Ranged, Rect, Renderable, Viewshed, AOE, MAPWIDTH, + Position, ProvidesHealing, Ranged, Rect, Renderable, SerializeMe, Viewshed, AOE, MAPWIDTH, }; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; +use specs::saveload::{MarkedBuilder, SimpleMarker}; /// Spawns the player and returns his/her entity object. pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { @@ -19,6 +20,7 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { .with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true }) .with(Name { name: "hero (you)".to_string() }) .with(CombatStats { max_hp: 30, hp: 30, defence: 2, power: 5 }) + .marked::>() .build() } @@ -64,6 +66,7 @@ fn monster(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharTy .with(Name { name: name.to_string() }) .with(BlocksTile {}) .with(CombatStats { max_hp: 16, hp: 16, defence: 1, power: 4 }) + .marked::>() .build(); } @@ -143,6 +146,7 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Destructible {}) .with(ProvidesHealing { amount: 12 }) + .marked::>() .build(); } @@ -160,6 +164,7 @@ fn weak_health_potion(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Destructible {}) .with(ProvidesHealing { amount: 6 }) + .marked::>() .build(); } @@ -177,6 +182,7 @@ fn poison_potion(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Destructible {}) .with(ProvidesHealing { amount: -12 }) + .marked::>() .build(); } @@ -197,6 +203,7 @@ fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) { .with(Destructible {}) .with(Ranged { range: 12 }) // Long range - as far as default vision range .with(InflictsDamage { amount: 10 }) // Low~ damage + .marked::>() .build(); } @@ -216,6 +223,7 @@ fn fireball_scroll(ecs: &mut World, x: i32, y: i32) { .with(Ranged { range: 10 }) .with(InflictsDamage { amount: 20 }) .with(AOE { radius: 3 }) + .marked::>() .build(); } @@ -234,5 +242,6 @@ fn confusion_scroll(ecs: &mut World, x: i32, y: i32) { .with(Destructible {}) .with(Ranged { range: 10 }) .with(Confusion { turns: 4 }) + .marked::>() .build(); }