diff --git a/src/components.rs b/src/components.rs index 8d6b22d..efbadf8 100644 --- a/src/components.rs +++ b/src/components.rs @@ -42,6 +42,8 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, + ShowSpawnMenu, + ShowTargeting { range : i32, item : Entity}, } #[derive(Component, Debug)] @@ -100,6 +102,7 @@ pub struct WantsToPickupItem { #[derive(Component, Debug)] pub struct WantsToUseItem { pub item: Entity, + pub target: Option } #[derive(Component, Debug, Clone)] pub struct WantsToDropItem { @@ -108,3 +111,23 @@ pub struct WantsToDropItem { #[derive(Component, Debug)] pub struct Consumable {} + +#[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 +} \ No newline at end of file diff --git a/src/gui.rs b/src/gui.rs index a707db4..f1f0a5a 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,8 +1,7 @@ use rltk::{RGB, Rltk, Point, VirtualKeyCode}; use specs::prelude::*; -use crate::{InBackpack, player}; -use super::{CombatStats, Player, GameLog, Map, Name, Position, State}; +use super::{CombatStats, Player, GameLog, Map, Name, Position, State, InBackpack, Viewshed, spawner}; fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { let map = ecs.fetch::(); @@ -131,6 +130,54 @@ pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Opti } +pub fn spawn_item(gs: &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { + let mut item_names : Vec = Vec::new(); + item_names.push("Health Potion".to_string()); + item_names.push("Magic Missle Scroll".to_string()); + item_names.push("Fireball Scroll".to_string()); + item_names.push("Confusion Scroll".to_string()); + + + let count = item_names.len(); + let mut y : i32 = (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), "Spawn which item?"); + ctx.print_color(18, y + count as i32 + 1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Escape to cancel"); + + let mut spawnable : Vec = Vec::new(); + let mut j = 0; + for item in item_names.iter() { + 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, &item.to_string()); + spawnable.push(item.to_string()); + y+=1; + j+=1; + } + + match ctx.key { + None => (ItemMenuResult::NoResponse, None), + Some(key) => { + match (key) { + VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None)}, + _ => { + let selection = rltk::letter_to_option(key); + let mouse_pos = ctx.mouse_pos(); + match selection { + 0 => {spawner::health_potion(&mut gs.ecs, mouse_pos.0, mouse_pos.1)}, + 1 => {spawner::magic_missile_scroll(&mut gs.ecs, mouse_pos.0, mouse_pos.1)}, + 2 => {spawner::fireball_scroll(&mut gs.ecs, mouse_pos.0, mouse_pos.1)}, + _ => {spawner::confusion_scroll(&mut gs.ecs, mouse_pos.0, mouse_pos.1)}, + } + (ItemMenuResult::Cancel, None) + } + } + } + } +} + pub fn drop_item_menu(gs: &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { let player_entity = gs.ecs.fetch::(); let names = gs.ecs.read_storage::(); @@ -174,4 +221,46 @@ pub fn drop_item_menu(gs: &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Optio } } +} + +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 viewsheds = gs.ecs.read_storage::(); + + ctx.print_color(5, 0, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Select target!"); + + // Highlight available target cells + let mut available_cells = Vec::new(); + let visible = viewsheds.get(*player_entity); + if let Some(visible) = visible { + // We have a viewshed component + 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); + } + + // Draw mouse cursor + 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) } \ No newline at end of file diff --git a/src/inventory_system.rs b/src/inventory_system.rs index 2100618..c811eff 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,8 +1,10 @@ use specs::prelude::*; +use crate::player; + use super::{ gamelog::GameLog, CombatStats, Consumable, InBackpack, Name, Position, ProvidesHealing, - WantsToDropItem, WantsToPickupItem, WantsToUseItem, + WantsToDropItem, WantsToPickupItem, WantsToUseItem, InflictsDamage, Map, SufferDamage, AreaOfEffect, Confusion }; pub struct ItemCollectionSystem {} @@ -51,37 +53,128 @@ impl<'a> System<'a> for ItemUseSystem { type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, + ReadExpect<'a, Map>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, ReadStorage<'a, Consumable>, ReadStorage<'a, ProvidesHealing>, WriteStorage<'a, CombatStats>, + ReadStorage<'a, InflictsDamage>, + WriteStorage<'a, SufferDamage>, + ReadStorage<'a, AreaOfEffect>, + WriteStorage<'a, Confusion> ); fn run(&mut self, data: Self::SystemData) { let ( player_entity, mut gamelog, + map, entities, mut wants_use, names, consumables, healing, mut combat_stats, + inflict_damage, + mut suffer_damage, + aoe, + mut confused, ) = data; - for (entity, useitem) in (&entities, &wants_use).join() { + for (entity, useitem,) in (&entities, &wants_use).join() { + let mut used_item = true; + + + + let mut targets : Vec = 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); + } + } + } + } + } + } + + let mut add_confusion = Vec::new(); + { + let causes_confusion = confused.get(useitem.item); + match causes_confusion { + None => {} + Some(confusion) => { + used_item = false; + for mob in targets.iter() { + add_confusion.push((*mob, confusion.turns)); + if entity == *player_entity { + let mob_name = names.get(*mob).unwrap(); + let item_name = names.get(useitem.item).unwrap(); + gamelog.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"); + } + let item_heals = healing.get(useitem.item); match item_heals { None => {} Some(healer) => { - let stats = combat_stats.get_mut(*player_entity); - stats.hp = i32::min(stats.max_hp, stats.hp + 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); + let item_damages = inflict_damage.get(useitem.item); + match item_damages { + None => {} + Some(damage) => { + used_item = false; + 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(); + let item_name = names.get(useitem.item).unwrap(); + gamelog.entries.push(format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, damage.damage)); + } + used_item = true; + } + } + } + + + let consumable = consumables.get(useitem.item); match consumable { None => {} Some(_) => { diff --git a/src/main.rs b/src/main.rs index 97f19fd..e62b8ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,8 +60,8 @@ impl State { pickup.run_now(&self.ecs); let mut drop_items = ItemDropSystem {}; drop_items.run_now(&self.ecs); - let mut potions = ItemUseSystem {}; - potions.run_now(&self.ecs); + let mut item_use = ItemUseSystem {}; + item_use.run_now(&self.ecs); self.ecs.maintain(); } } @@ -127,16 +127,24 @@ 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("Failed 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("Failed to insert intent"); + newrunstate = RunState::PlayerTurn; + } + } } } @@ -176,6 +184,28 @@ impl GameState for State { } } } + 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; + } + } + + } + RunState::ShowSpawnMenu => { + let result = gui::spawn_item(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => {newrunstate = RunState::AwaitingInput;} + } + + } } { @@ -209,7 +239,12 @@ 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::(); 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 ad3ed63..c9d162a 100644 --- a/src/monster_ai_system.rs +++ b/src/monster_ai_system.rs @@ -1,4 +1,4 @@ -use super::{Map, Monster, Position, Viewshed, WantsToMelee, RunState}; +use super::{Map, Monster, Position, Viewshed, WantsToMelee, RunState, Confusion}; use rltk::{Point}; use specs::prelude::*; @@ -16,39 +16,55 @@ 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) { - let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee) = data; + let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee, mut confused) = data; if *runstate != RunState::MonsterTurn { return; } for (entity, mut viewshed, _monster, mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() { - if viewshed.visible_tiles.contains(&*player_pos) { - let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); - if distance < 1.5 { - wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack"); + let mut can_act = true; + + let is_confused = confused.get_mut(entity); + if let Some(i_am_confused) = is_confused { + i_am_confused.turns -= 1; + if i_am_confused.turns < 1 { + confused.remove(entity); } - else if viewshed.visible_tiles.contains(&*player_pos) { - // Path to the player - let path = rltk::a_star_search( - map.xy_idx(pos.x, pos.y), - map.xy_idx(player_pos.x, player_pos.y), - &mut *map - ); - if path.success && path.steps.len() > 1 { - let mut idx = map.xy_idx(pos.x, pos.y); - map.blocked[idx] = false; - pos.x = path.steps[1] as i32 % map.width; - pos.y = path.steps[1] as i32 / map.width; - idx = map.xy_idx(pos.x, pos.y); - map.blocked[idx] = true; - viewshed.dirty = true; - } - } - + can_act = false; } + + if can_act { + if viewshed.visible_tiles.contains(&*player_pos) { + let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); + if distance < 1.5 { + wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack"); + } + else if viewshed.visible_tiles.contains(&*player_pos) { + // Path to the player + let path = rltk::a_star_search( + map.xy_idx(pos.x, pos.y), + map.xy_idx(player_pos.x, player_pos.y), + &mut *map + ); + if path.success && path.steps.len() > 1 { + let mut idx = map.xy_idx(pos.x, pos.y); + map.blocked[idx] = false; + pos.x = path.steps[1] as i32 % map.width; + pos.y = path.steps[1] as i32 / map.width; + idx = map.xy_idx(pos.x, pos.y); + map.blocked[idx] = true; + viewshed.dirty = true; + } + } + + } + } + + } } } diff --git a/src/player.rs b/src/player.rs index e5c71bd..c0e0cff 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,4 +1,6 @@ -use super::{Map, Player, Position, RunState, State, Viewshed, CombatStats, WantsToMelee, Item, GameLog, WantsToPickupItem}; +use crate::spawn_item; + +use super::{Map, Player, Position, RunState, State, Viewshed, CombatStats, WantsToMelee, Item, GameLog, WantsToPickupItem, spawner}; use rltk::{Point, Rltk, VirtualKeyCode}; use specs::prelude::*; use std::cmp::{max, min}; @@ -80,13 +82,19 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { try_move_player(0, 1, &mut gs.ecs) } - VirtualKeyCode::Numpad9 | VirtualKeyCode::Y => {try_move_player(1, -1, &mut gs.ecs)} - VirtualKeyCode::Numpad7 | VirtualKeyCode::U => {try_move_player(-1, -1, &mut gs.ecs)} + VirtualKeyCode::Numpad9 | VirtualKeyCode::U => {try_move_player(1, -1, &mut gs.ecs)} + VirtualKeyCode::Numpad7 | VirtualKeyCode::Y => {try_move_player(-1, -1, &mut gs.ecs)} VirtualKeyCode::Numpad3 | VirtualKeyCode::N => {try_move_player(1, 1, &mut gs.ecs)} VirtualKeyCode::Numpad1 | VirtualKeyCode::B => {try_move_player(-1, 1, &mut gs.ecs)} VirtualKeyCode::G => get_item(&mut gs.ecs), VirtualKeyCode::I => return RunState::ShowInventory, VirtualKeyCode::D => return RunState::ShowDropItem, + VirtualKeyCode::Minus => {return RunState::ShowSpawnMenu} + VirtualKeyCode::NumpadAdd => { + let mouse_pos = ctx.mouse_pos(); + spawner::health_potion(&mut gs.ecs, mouse_pos.0, mouse_pos.1); + return RunState::AwaitingInput; + } _ => return RunState::AwaitingInput, }, } diff --git a/src/spawner.rs b/src/spawner.rs index 055f437..07d970b 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,8 +1,8 @@ -use crate::MAPWIDTH; +use crate::AreaOfEffect; use super::{ BlocksTile, CombatStats, Item, Monster, Name, Player, Position, ProvidesHealing, Rect, - Renderable, Viewshed, + Renderable, Viewshed, InflictsDamage, MAPWIDTH, Ranged, Consumable, Confusion }; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; @@ -53,6 +53,20 @@ pub fn random_monster(ecs: &mut World, x: i32, y: i32) { } } +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) } + } +} + fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('o'), "Orc"); } @@ -133,11 +147,11 @@ pub fn spawn_room(ecs: &mut World, room: &Rect) { for idx in item_spawn_points.iter() { let x = *idx % MAPWIDTH; let y = *idx / MAPWIDTH; - health_potion(ecs, x as i32, y as i32); + random_item(ecs, x as i32, y as i32); } } -fn health_potion(ecs: &mut World, x: i32, y: i32) { +pub fn health_potion(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position { x, y }) .with(Renderable { @@ -150,6 +164,59 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) { name: "Health Potion".to_string(), }) .with(Item {}) + .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::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 }) + .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::PINK), + 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(); +} \ No newline at end of file