Implement ranged item attacks

This commit is contained in:
Timothy Warren 2021-11-05 10:42:44 -04:00
parent ef8d51de1f
commit d2ebe5dc1d
5 changed files with 201 additions and 26 deletions

View File

@ -108,6 +108,7 @@ pub struct WantsToPickupItem {
#[derive(Component, Debug)]
pub struct WantsToUseItem {
pub item: Entity,
pub target: Option<rltk::Point>,
}
#[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,
}

View File

@ -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<Point>) {
let player_entity = gs.ecs.fetch::<Entity>();
let player_pos = gs.ecs.fetch::<Point>();
let viewsheds = gs.ecs.read_storage::<Viewshed>();
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)
}

View File

@ -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,9 +94,38 @@ impl<'a> System<'a> for ItemUseSystem {
healer.heal_amount
));
}
used_item = true;
}
}
// If it inflicts damage, apply it to the target cell
match inflict_damage.get(useitem.item) {
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() {
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 => {}
@ -98,6 +136,7 @@ impl<'a> System<'a> for ItemUseSystem {
}
}
}
}
wants_use.clear();
}

View File

@ -48,6 +48,7 @@ pub enum RunState {
MonsterTurn,
ShowInventory,
ShowDropItem,
ShowTargeting { range: i32, item: Entity },
}
pub struct State {
@ -144,12 +145,22 @@ impl GameState for State {
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let is_ranged = self.ecs.read_storage::<Ranged>();
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::<WantsToUseItem>();
intent
.insert(
*self.ecs.fetch::<Entity>(),
WantsToUseItem {
item: item_entity,
target: None,
},
)
.expect("failed to add intent to use item");
@ -158,6 +169,7 @@ impl GameState for State {
}
}
}
}
RunState::ShowDropItem => {
let result = gui::drop_item_menu(self, ctx);
@ -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::<WantsToUseItem>();
intent
.insert(
*self.ecs.fetch::<Entity>(),
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();

View File

@ -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::<RandomNumberGenerator>();
roll = rng.roll_dice(1, 2);
}
match roll {
1 => health_potion(ecs, x, y),
_ => magic_missile_scroll(ecs, x, y),
}
}