Implement ranged item attacks
This commit is contained in:
parent
ef8d51de1f
commit
d2ebe5dc1d
@ -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,
|
||||
}
|
||||
|
63
src/gui.rs
63
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<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)
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
36
src/main.rs
36
src/main.rs
@ -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();
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user