diff --git a/src/components.rs b/src/components.rs index eafc560..2018288 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,4 @@ -use rltk::RGB; +use rltk::{Point, RGB}; use specs::prelude::*; use specs_derive::*; @@ -88,8 +88,9 @@ pub struct WantsToPickupItem { } #[derive(Component, Debug, Clone)] -pub struct WantsToDrinkPotion { - pub potion: Entity, +pub struct WantsToUseItem { + pub item: Entity, + pub target: Option, } #[derive(Component, Debug, Clone)] @@ -97,4 +98,30 @@ pub struct WantsToDropItem { pub item: Entity, } +#[derive(Component, Debug)] +pub struct Consumable {} +#[derive(Component, Debug)] +pub struct ProvidesHealing { + pub heal_amount: i32, +} + +#[derive(Component, Debug)] +pub struct Ranged { + pub range: i32, +} + +#[derive(Component, Debug)] +pub struct InflictsDamage { + pub damage: i32, +} + +#[derive(Component, Debug)] +pub struct AreaOfEffect { + pub radius: i32, +} + +#[derive(Component, Debug)] +pub struct Confusion { + pub turns: i32, +} diff --git a/src/gui.rs b/src/gui.rs index f301289..bfa1175 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,14 +1,14 @@ -use rltk::{BTerm, Point, VirtualKeyCode}; -use rltk::RGB; use rltk::Rltk; +use rltk::RGB; +use rltk::{BTerm, Point, VirtualKeyCode}; use specs::prelude::*; -use crate::{CombatStats, InBackpack, State}; use crate::gamelog::GameLog; use crate::Map; use crate::Name; use crate::Player; use crate::Position; +use crate::{CombatStats, InBackpack, State, Viewshed}; pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { ctx.draw_box( @@ -230,18 +230,19 @@ pub fn show_inventory(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option 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) + 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) } - } + }, } } @@ -251,20 +252,62 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option let backpack = gs.ecs.read_storage::(); let entities = gs.ecs.entities(); - let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity); + 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), "Drop Which Item?"); - ctx.print_color(18, y + count as i32 + 1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel"); + 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), + "Drop 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 = 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(')')); + 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); @@ -274,17 +317,74 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option 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) + 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( + gs: &mut State, + ctx: &mut Rltk, + range: i32, +) -> (ItemMenuResult, Option) { + let player_entity = gs.ecs.fetch::(); + let player_pos = gs.ecs.fetch::(); + let viewshed = gs.ecs.read_storage::(); + + ctx.print_color( + 5, + 0, + RGB::named(rltk::YELLOW), + RGB::named(rltk::BLACK), + "Select Target:", + ); + + let mut available_cells = Vec::new(); + let visible = viewshed.get(*player_entity); + if let Some(visible) = visible { + for idx in visible.visible_tiles.iter() { + let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx); + if distance <= range as f32 { + ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE)); + available_cells.push(idx); } } + } else { + return (ItemMenuResult::Cancel, None); } -} \ No newline at end of file + + let mouse_pos = ctx.mouse_pos(); + let mut valid_target = false; + for idx in available_cells.iter() { + if idx.x == mouse_pos.0 && idx.y == mouse_pos.1 { + valid_target = true; + } + } + if valid_target { + ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN)); + if ctx.left_click { + return ( + ItemMenuResult::Selected, + Some(Point::new(mouse_pos.0, mouse_pos.1)), + ); + } + } else { + ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED)); + if ctx.left_click { + return (ItemMenuResult::Cancel, None); + } + } + + (ItemMenuResult::NoResponse, None) +} diff --git a/src/inventory_system.rs b/src/inventory_system.rs index aa9ccee..2222e83 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,9 +1,10 @@ use rltk::Rltk; use specs::prelude::*; -use crate::{CombatStats, InBackpack, Name, Position, Potion, State, WantsToDrinkPotion, WantsToDropItem, WantsToPickupItem}; +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}; pub struct ItemCollectionSystem {} @@ -44,37 +45,136 @@ impl<'a> System<'a> for ItemCollectionSystem { } } -pub struct PotionUseSystem {} +pub struct ItemUseSystem {} -impl<'a> System<'a> for PotionUseSystem { +impl<'a> System<'a> for ItemUseSystem { type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, Entities<'a>, - WriteStorage<'a, WantsToDrinkPotion>, + WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, - ReadStorage<'a, Potion>, - WriteStorage<'a, CombatStats> + ReadStorage<'a, Consumable>, + WriteStorage<'a, CombatStats>, + ReadStorage<'a, ProvidesHealing>, + ReadStorage<'a, InflictsDamage>, + ReadStorage<'a, Ranged>, + ReadExpect<'a, Map>, + WriteStorage<'a, SufferDamage>, + ReadStorage<'a, AreaOfEffect>, + WriteStorage<'a, Confusion>, ); fn run(&mut self, data: Self::SystemData) { - let (player_entity, mut gamelog, entities, mut wants_drink, names, potions, mut combat_stats) = data; + let ( + player_entity, + mut game_log, + entities, + mut wants_use, + names, + consumables, + mut combat_stats, + healing, + inflicts_damage, + ranged, + map, + mut suffer_damage, + aoe, + mut confused + ) = data; - for (entity, drink, stats) in (&entities, &wants_drink, &mut combat_stats).join() { - let potion = potions.get(drink.potion); - match potion { - None => {} - Some(potion) => { - stats.hp = i32::min(stats.max_hp, stats.hp + potion.heal_amount); - if entity == *player_entity { - gamelog.entries.push(format!("You drink the {}, healing {} hp.", names.get(drink.potion).unwrap().name, potion.heal_amount)); + for (entity, use_item) in (&entities, &wants_use).join() { + let mut used_item = true; + + let mut targets: Vec = Vec::new(); + match use_item.target { + None => { targets.push(*player_entity) } + Some(target) => { + let area_effect = aoe.get(use_item.item); + match area_effect { + None => { + let idx = map.xy_idx(target.x, target.y); + for mob in map.tile_content[idx].iter() { + targets.push(*mob); + } + } + 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); + 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() { + targets.push(*mob); + } + } + } + } + } + } + + if let Some(item_damages) = inflicts_damage.get(use_item.item) { + used_item = false; + for mob in targets.iter() { + SufferDamage::new_damage(&mut suffer_damage, *mob, item_damages.damage); + 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)); + } + + used_item = true; + } + } + + if let Some(item_heals) = healing.get(use_item.item) { + used_item = false; + 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 + item_heals.heal_amount); + if entity == *player_entity { + game_log.entries.push(format!( + "You drink the {}, healing {} hp.", + names.get(use_item.item).unwrap().name, + item_heals.heal_amount + )); + } + used_item = true; + } + } + } + + let mut add_confusion = Vec::new(); + { + if let Some(caused_confusion) = confused.get(use_item.item) { + used_item = false; + for mob in targets.iter() { + add_confusion.push((*mob, caused_confusion.turns)); + 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)); + } + } + } + } + for mob in add_confusion.iter() { + 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 { + None => {} + Some(_) => { + entities.delete(use_item.item).expect("Delete failed"); } - entities.delete(drink.potion).expect("Delete failed"); } } } - wants_drink.clear(); + wants_use.clear(); } } @@ -88,11 +188,19 @@ impl<'a> System<'a> for ItemDropSystem { WriteStorage<'a, WantsToDropItem>, ReadStorage<'a, Name>, WriteStorage<'a, Position>, - WriteStorage<'a, InBackpack> + WriteStorage<'a, InBackpack>, ); fn run(&mut self, data: Self::SystemData) { - let (player_entity, mut gamelog, entities, mut wants_drop, names, mut positions, mut backpack) = data; + let ( + player_entity, + mut game_log, + entities, + mut wants_drop, + names, + mut positions, + mut backpack, + ) = data; for (entity, to_drop) in (&entities, &wants_drop).join() { let mut dropper_pos: Position = Position { x: 0, y: 0 }; @@ -101,16 +209,25 @@ impl<'a> System<'a> for ItemDropSystem { dropper_pos.x = dropped_pos.x; dropper_pos.y = dropped_pos.y; } - positions.insert(to_drop.item, Position { x: dropper_pos.x, y: dropper_pos.y }).expect("Unable to insert position"); + positions + .insert( + to_drop.item, + Position { + x: dropper_pos.x, + y: dropper_pos.y, + }, + ) + .expect("Unable to insert position"); backpack.remove(to_drop.item); if entity == *player_entity { - gamelog.entries.push(format!("You drop the {}.", names.get(to_drop.item).unwrap().name)); + game_log.entries.push(format!( + "You drop the {}.", + names.get(to_drop.item).unwrap().name + )); } } wants_drop.clear(); } } - - diff --git a/src/main.rs b/src/main.rs index a961221..9c775ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use monster_ai_system::*; use player::*; use visibility_system::*; -use crate::inventory_system::{ItemCollectionSystem, ItemDropSystem, PotionUseSystem}; +use crate::inventory_system::{ItemCollectionSystem, ItemDropSystem, ItemUseSystem}; mod components; mod damage_system; @@ -36,6 +36,7 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, + ShowTargeting { range: i32, item: Entity }, } pub struct State { @@ -62,7 +63,7 @@ impl State { let mut inventory = ItemCollectionSystem {}; inventory.run_now(&self.ecs); - let mut potions = PotionUseSystem {}; + let mut potions = ItemUseSystem {}; potions.run_now(&self.ecs); let mut drop_items = ItemDropSystem {}; @@ -122,9 +123,23 @@ impl GameState for State { gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); - let mut intent = self.ecs.write_storage::(); - intent.insert(*self.ecs.fetch::(), WantsToDrinkPotion { potion: item_entity }).expect("Unable to insert intent"); - newrunstate = RunState::PlayerTurn; + 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 { + range: is_item_ranged.range, + item: item_entity, + } + } else { + let mut intent = self.ecs.write_storage::(); + intent + .insert( + *self.ecs.fetch::(), + WantsToUseItem { item: item_entity, target: None }, + ) + .expect("Unable to insert intent"); + newrunstate = RunState::PlayerTurn; + } } } } @@ -136,7 +151,24 @@ impl GameState for State { gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); let mut intent = self.ecs.write_storage::(); - intent.insert(*self.ecs.fetch::(), WantsToDropItem { item: item_entity }).expect("Unable to insert intent"); + intent + .insert( + *self.ecs.fetch::(), + WantsToDropItem { item: item_entity }, + ) + .expect("Unable to insert intent"); + newrunstate = RunState::PlayerTurn; + } + } + } + RunState::ShowTargeting { range, item } => { + let result = gui::ranged_target(self, ctx, range); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let mut intent = self.ecs.write_storage::(); + intent.insert(*self.ecs.fetch::(), WantsToUseItem { item, target: result.1 }).expect("Unable to insert intent"); newrunstate = RunState::PlayerTurn; } } @@ -177,12 +209,17 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); - gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); - gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); let map = Map::new_map_rooms_and_corridors(); let (player_x, player_y) = map.rooms[0].center(); diff --git a/src/monster_ai_system.rs b/src/monster_ai_system.rs index 02b3793..5079ac4 100644 --- a/src/monster_ai_system.rs +++ b/src/monster_ai_system.rs @@ -1,11 +1,7 @@ use rltk::{console, Point}; use specs::prelude::*; -use crate::{ - components::{Monster, Position, Viewshed, WantsToMelee}, - map::Map, - RunState, -}; +use crate::{components::{Monster, Position, Viewshed, WantsToMelee}, Confusion, map::Map, RunState}; pub struct MonsterAI {} @@ -21,6 +17,7 @@ impl<'a> System<'a> for MonsterAI { ReadStorage<'a, Monster>, WriteStorage<'a, Position>, WriteStorage<'a, WantsToMelee>, + WriteStorage<'a, Confusion>, ); fn run(&mut self, data: Self::SystemData) { @@ -34,6 +31,7 @@ impl<'a> System<'a> for MonsterAI { monster, mut position, mut wants_to_melee, + mut confused, ) = data; if *runstate != RunState::MonsterTurn { @@ -41,8 +39,22 @@ 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; + + if let Some(i_am_confused) = confused.get_mut(entity) { + i_am_confused.turns -= 1; + if i_am_confused.turns < 1 { + confused.remove(entity); + } + can_act = false; + } + + if !can_act { + return; + } + let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); if distance < 1.5 { diff --git a/src/player.rs b/src/player.rs index 44013ec..df6485c 100644 --- a/src/player.rs +++ b/src/player.rs @@ -3,12 +3,12 @@ use std::cmp::{max, min}; use rltk::{console, Point, Rltk, VirtualKeyCode}; use specs::prelude::*; +use crate::gamelog::GameLog; use crate::{ components::{CombatStats, Player, Position, Viewshed, WantsToMelee}, - Item, - map::{Map, TileType}, RunState, State, WantsToPickupItem, + map::{Map, TileType}, + Item, RunState, State, WantsToPickupItem, }; -use crate::gamelog::GameLog; pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { let mut positions = ecs.write_storage::(); @@ -20,7 +20,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { let map = ecs.fetch::(); for (entity, _player, pos, viewshed) in - (&entities, &mut players, &mut positions, &mut viewsheds).join() + (&entities, &mut players, &mut positions, &mut viewsheds).join() { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); diff --git a/src/spawner.rs b/src/spawner.rs index c7c6f80..ce368bc 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,10 +1,7 @@ use rltk::{FontCharType, RandomNumberGenerator, RGB}; use specs::prelude::*; -use crate::{ - BlocksTile, CombatStats, Item, MAP_WIDTH, MAX_ITEMS, MAX_MONSTER, Monster, Name, Player, Position, - Potion, Renderable, Viewshed, -}; +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::rect::Rect; pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { @@ -130,7 +127,7 @@ pub fn spawn_room(ecs: &mut World, room: &Rect) { for idx in item_spawn_points.iter() { let x = *idx % MAP_WIDTH; let y = *idx / MAP_WIDTH; - health_potion(ecs, x as i32, y as i32); + random_item(ecs, x as i32, y as i32); } } @@ -147,6 +144,80 @@ pub fn health_potion(ecs: &mut World, x: i32, y: i32) { name: "Health Potion".to_string(), }) .with(Item {}) - .with(Potion { heal_amount: 8 }) + .with(Consumable {}) + .with(ProvidesHealing { heal_amount: 8 }) .build(); } + +pub fn magic_missile_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::CYAN), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { + name: "Magic Missile Scroll".to_string(), + }) + .with(Item {}) + .with(Consumable {}) + .with(Ranged { range: 6 }) + .with(InflictsDamage { damage: 8 }) + .build(); +} + +pub 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::CYAN), + 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 }) + .build(); +} + +pub fn confusion_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::CYAN), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { + name: "Confusion Scroll".to_string(), + }) + .with(Item {}) + .with(Consumable {}) + .with(Ranged { range: 6 }) + .with(Confusion { turns: 4 }) + .build(); +} + + +pub fn random_item(ecs: &mut World, x: i32, y: i32) { + let roll: i32; + { + let mut rng = ecs.write_resource::(); + roll = rng.roll_dice(1, 4); + } + match roll { + 1 => health_potion(ecs, x, y), + 2 => fireball_scroll(ecs, x, y), + 3 => confusion_scroll(ecs, x, y), + _ => magic_missile_scroll(ecs, x, y), + } +}