From d2ebe5dc1db9024315d060dd638f79929cdd9b5c Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Fri, 5 Nov 2021 10:42:44 -0400 Subject: [PATCH] Implement ranged item attacks --- src/components.rs | 11 +++++++ src/gui.rs | 63 ++++++++++++++++++++++++++++++++++++++++- src/inventory_system.rs | 55 +++++++++++++++++++++++++++++------ src/main.rs | 56 +++++++++++++++++++++++++++++------- src/spawner.rs | 42 ++++++++++++++++++++++----- 5 files changed, 201 insertions(+), 26 deletions(-) diff --git a/src/components.rs b/src/components.rs index 71bdfe4..a10267f 100644 --- a/src/components.rs +++ b/src/components.rs @@ -108,6 +108,7 @@ pub struct WantsToPickupItem { #[derive(Component, Debug)] pub struct WantsToUseItem { pub item: Entity, + pub target: Option, } #[derive(Component, Debug, Clone)] @@ -117,3 +118,13 @@ 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, +} diff --git a/src/gui.rs b/src/gui.rs index 9f5ed43..6640b63 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,4 +1,6 @@ -use crate::{game_log::GameLog, CombatStats, InBackpack, Map, Name, Player, Position, State}; +use crate::{ + game_log::GameLog, CombatStats, InBackpack, Map, Name, Player, Position, State, Viewshed, +}; use rltk::{Point, Rltk, VirtualKeyCode, RGB}; use specs::prelude::*; @@ -334,3 +336,62 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option }, } } + +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 { + 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) +} diff --git a/src/inventory_system.rs b/src/inventory_system.rs index 6967b8d..a08f658 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,6 +1,6 @@ use crate::{ - game_log::GameLog, CombatStats, Consumable, InBackpack, Map, Name, Position, ProvidesHealing, - WantsToDropItem, WantsToPickupItem, WantsToUseItem, + game_log::GameLog, CombatStats, Consumable, InBackpack, InflictsDamage, Map, Name, Position, + ProvidesHealing, SufferDamage, WantsToDropItem, WantsToPickupItem, WantsToUseItem, }; use specs::prelude::*; @@ -57,7 +57,9 @@ impl<'a> System<'a> for ItemUseSystem { ReadStorage<'a, Name>, ReadStorage<'a, Consumable>, ReadStorage<'a, ProvidesHealing>, + ReadStorage<'a, InflictsDamage>, WriteStorage<'a, CombatStats>, + WriteStorage<'a, SufferDamage>, ); fn run(&mut self, data: Self::SystemData) { @@ -70,13 +72,20 @@ impl<'a> System<'a> for ItemUseSystem { names, consumables, healing, + inflict_damage, mut combat_stats, + mut suffer_damage, ) = data; for (entity, useitem, stats) in (&entities, &wants_use, &mut combat_stats).join() { + let mut used_item = true; + + // If the item heals, apply the healing match healing.get(useitem.item) { None => {} Some(healer) => { + used_item = false; + stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount); if entity == *player_entity { gamelog.entries.push(format!( @@ -85,16 +94,46 @@ impl<'a> System<'a> for ItemUseSystem { healer.heal_amount )); } + + used_item = true; } } - let consumable = consumables.get(useitem.item); - match consumable { + // If it inflicts damage, apply it to the target cell + match inflict_damage.get(useitem.item) { None => {} - Some(_) => { - entities - .delete(useitem.item) - .expect("Failed to consume item"); + 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() { + 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; + } + } + } + + // If it's a consumable, delete it on use + if used_item { + let consumable = consumables.get(useitem.item); + match consumable { + None => {} + Some(_) => { + entities + .delete(useitem.item) + .expect("Failed to consume item"); + } } } } diff --git a/src/main.rs b/src/main.rs index 7e4437f..c536ecc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, + ShowTargeting { range: i32, item: Entity }, } pub struct State { @@ -144,17 +145,28 @@ 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::(), - WantsToUseItem { - item: item_entity, - }, - ) - .expect("failed to add intent to use item"); + let is_ranged = self.ecs.read_storage::(); + let is_item_ranged = is_ranged.get(item_entity); - newrunstate = RunState::PlayerTurn; + 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 add intent to use item"); + + newrunstate = RunState::PlayerTurn; + } } } } @@ -178,6 +190,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("failed to add intent to use item"); + + newrunstate = RunState::PlayerTurn; + } + } + } } { @@ -217,6 +251,8 @@ fn main() -> rltk::BError { WantsToUseItem, WantsToDropItem, Consumable, + Ranged, + InflictsDamage, ); let map = Map::new_map_rooms_and_corridors(); diff --git a/src/spawner.rs b/src/spawner.rs index 37814b4..2f43687 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,6 +1,6 @@ use crate::{ - BlocksTile, CombatStats, Consumable, Item, Monster, Name, Player, Position, ProvidesHealing, - Rect, Renderable, Viewshed, MAP_WIDTH, + BlocksTile, CombatStats, Consumable, InflictsDamage, Item, Monster, Name, Player, Position, + ProvidesHealing, Ranged, Rect, Renderable, Viewshed, MAP_WIDTH, }; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; @@ -126,12 +126,12 @@ pub fn spawn_room(ecs: &mut World, room: &Rect) { random_monster(ecs, x as i32, y as i32); } - // Actually spawn the potions + // Actually spawn the items 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); } } @@ -144,11 +144,39 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) { bg: RGB::named(rltk::BLACK), render_order: 2, }) - .with(Name { - name: "Health Potion".to_string(), - }) + .with(Name::new("Health Potion")) .with(Item {}) .with(Consumable {}) .with(ProvidesHealing { heal_amount: 8 }) .build(); } + +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::new("Magic Missile Scroll")) + .with(Item {}) + .with(Consumable {}) + .with(Ranged { range: 6 }) + .with(InflictsDamage { damage: 8 }) + .build(); +} + +fn random_item(ecs: &mut World, x: i32, y: i32) { + let roll: i32; + { + let mut rng = ecs.write_resource::(); + roll = rng.roll_dice(1, 2); + } + + match roll { + 1 => health_potion(ecs, x, y), + _ => magic_missile_scroll(ecs, x, y), + } +}