From 06c3d40c656da92e7e659cd40f27e439497e4564 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sun, 9 Jul 2023 09:12:21 +0100 Subject: [PATCH] magic missile, fireball scrolls --- src/components.rs | 18 ++++- src/damage_system.rs | 10 ++- src/gui.rs | 56 +++++++++++++- src/inventory_system.rs | 159 +++++++++++++++++++++++++++++++--------- src/main.rs | 49 +++++++++++-- src/particle_system.rs | 24 ++++++ src/spawner.rs | 49 +++++++++++-- 7 files changed, 315 insertions(+), 50 deletions(-) diff --git a/src/components.rs b/src/components.rs index c8a0012..3d01d36 100644 --- a/src/components.rs +++ b/src/components.rs @@ -71,7 +71,22 @@ pub struct Item {} #[derive(Component, Debug)] pub struct ProvidesHealing { - pub heal_amount: i32, + pub amount: i32, +} + +#[derive(Component, Debug)] +pub struct InflictsDamage { + pub amount: i32, +} + +#[derive(Component, Debug)] +pub struct Ranged { + pub range: i32, +} + +#[derive(Component, Debug)] +pub struct AOE { + pub radius: i32, } #[derive(Component, Debug, Clone)] @@ -93,6 +108,7 @@ pub struct WantsToDropItem { #[derive(Component, Debug)] pub struct WantsToUseItem { pub item: Entity, + pub target: Option, } #[derive(Component, Debug)] diff --git a/src/damage_system.rs b/src/damage_system.rs index e58755f..5551bd1 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -1,4 +1,4 @@ -use super::{gamelog::GameLog, CombatStats, Entities, Map, Name, Player, Position, SufferDamage}; +use super::{gamelog::GameLog, CombatStats, Entities, Item, Map, Name, Player, Position, SufferDamage}; use specs::prelude::*; pub struct DamageSystem {} @@ -35,6 +35,7 @@ pub fn delete_the_dead(ecs: &mut World) { let combat_stats = ecs.read_storage::(); let players = ecs.read_storage::(); let names = ecs.read_storage::(); + let items = ecs.read_storage::(); let entities = ecs.entities(); let mut log = ecs.write_resource::(); for (entity, stats) in (&entities, &combat_stats).join() { @@ -44,7 +45,12 @@ pub fn delete_the_dead(ecs: &mut World) { None => { let victim_name = names.get(entity); if let Some(victim_name) = victim_name { - log.entries.push(format!("{} died!", &victim_name.name)); + let item = items.get(entity); + if let Some(_item) = item { + log.entries.push(format!("{} was destroyed!", &victim_name.name)); + } else { + log.entries.push(format!("{} died!", &victim_name.name)); + } } dead.push(entity) } diff --git a/src/gui.rs b/src/gui.rs index 9fada45..4658f7e 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,6 +1,6 @@ use super::{ gamelog::GameLog, rex_assets::RexAssets, CombatStats, InBackpack, Map, Name, Player, Point, Position, RunState, - State, + State, Viewshed, }; use rltk::{Rltk, VirtualKeyCode, RGB}; use specs::prelude::*; @@ -205,6 +205,60 @@ 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, aoe: 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 cells + let mut available_cells = Vec::new(); + let visible = viewsheds.get(*player_entity); + if let Some(visible) = visible { + // We have a viewshed + 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 map = gs.ecs.fetch::(); + 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 { + if aoe > 0 { + let mut blast_tiles = rltk::field_of_view(Point::new(mouse_pos.0, mouse_pos.1), aoe, &*map); + blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1); + for tile in blast_tiles.iter() { + ctx.set_bg(tile.x, tile.y, RGB::named(rltk::DARKCYAN)); + } + } + 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) +} + #[derive(PartialEq, Copy, Clone)] pub enum MainMenuSelection { NewGame, diff --git a/src/inventory_system.rs b/src/inventory_system.rs index ce40b12..2cfbdcf 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,6 +1,6 @@ use super::{ - gamelog::GameLog, CombatStats, Consumable, InBackpack, Name, ParticleBuilder, Position, ProvidesHealing, - WantsToDropItem, WantsToPickupItem, WantsToUseItem, DEFAULT_PARTICLE_LIFETIME, + gamelog::GameLog, CombatStats, Consumable, InBackpack, InflictsDamage, Map, Name, ParticleBuilder, Position, + ProvidesHealing, SufferDamage, WantsToDropItem, WantsToPickupItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, }; use specs::prelude::*; @@ -39,65 +39,156 @@ 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>, + WriteStorage<'a, SufferDamage>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, + ReadStorage<'a, InflictsDamage>, + ReadStorage<'a, AOE>, ); fn run(&mut self, data: Self::SystemData) { let ( player_entity, mut gamelog, + map, entities, - mut wants_use, + mut wants_to_use, names, consumables, - healing, + provides_healing, mut combat_stats, + mut suffer_damage, mut particle_builder, positions, + inflicts_damage, + aoe, ) = data; - for (entity, use_item, stats) in (&entities, &wants_use, &mut combat_stats).join() { - let item_heals = healing.get(use_item.item); - match item_heals { - None => {} - Some(healer) => { - stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount); - let pos = positions.get(entity); - if let Some(pos) = pos { - particle_builder.request( - pos.x, - pos.y, - rltk::RGB::named(rltk::GREEN), - rltk::RGB::named(rltk::BLACK), - rltk::to_cp437('♥'), - DEFAULT_PARTICLE_LIFETIME, - ); - } - if entity == *player_entity { - gamelog.entries.push(format!( - "You quaff the {}, and heal {} hp.", - names.get(use_item.item).unwrap().name, - healer.heal_amount - )); - } - let consumable = consumables.get(use_item.item); - match consumable { - None => {} - Some(_) => { - entities.delete(use_item.item).expect("Delete failed"); + for (entity, wants_to_use) in (&entities, &wants_to_use).join() { + let mut used_item = true; + let mut aoe_item = false; + let item_being_used = names.get(wants_to_use.item).unwrap(); + + // TARGETING + let mut targets: Vec = Vec::new(); + match wants_to_use.target { + None => { + targets.push(*player_entity); + } + Some(target) => { + let area_effect = aoe.get(wants_to_use.item); + match area_effect { + None => { + // Single target in a 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 + aoe_item = true; + 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); + } + particle_builder.request( + tile_idx.x, + tile_idx.y, + rltk::RGB::named(rltk::ORANGE), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('░'), + 200.0, + ); + } } } } } + + // HEALING ITEM + let item_heals = provides_healing.get(wants_to_use.item); + match item_heals { + None => {} + Some(heal) => { + 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 + heal.amount); + if entity == *player_entity { + gamelog.entries.push(format!( + "You quaff the {}, and heal {} hp.", + item_being_used.name, heal.amount + )); + } + let pos = positions.get(entity); + if let Some(pos) = pos { + particle_builder.request( + pos.x, + pos.y, + rltk::RGB::named(rltk::GREEN), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('♥'), + DEFAULT_PARTICLE_LIFETIME, + ); + } + } + } + } + } + + // DAMAGING ITEM + let item_damages = inflicts_damage.get(wants_to_use.item); + match item_damages { + None => {} + Some(damage) => { + let target_point = wants_to_use.target.unwrap(); + gamelog.entries.push(format!("You use the {}!", item_being_used.name)); + if !aoe_item { + particle_builder.request_star( + target_point.x, + target_point.y, + rltk::RGB::named(rltk::CYAN), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('*'), + DEFAULT_PARTICLE_LIFETIME, + ); + } + for mob in targets.iter() { + SufferDamage::new_damage(&mut suffer_damage, *mob, damage.amount); + if entity == *player_entity { + let mob_name = names.get(*mob).unwrap(); + gamelog.entries.push(format!( + "{} takes {} damage from the {}!", + mob_name.name, damage.amount, item_being_used.name + )); + } + + used_item = true; + } + } + } + if used_item { + let consumable = consumables.get(wants_to_use.item); + match consumable { + None => {} + Some(_) => { + entities.delete(wants_to_use.item).expect("Delete failed"); + } + } + } } - wants_use.clear(); + wants_to_use.clear(); } } diff --git a/src/main.rs b/src/main.rs index 5844f8e..b1b6c0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,7 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, + ShowTargeting { range: i32, item: Entity, aoe: i32 }, MainMenu { menu_selection: gui::MainMenuSelection }, } @@ -139,11 +140,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("Unable to insert intent."); - new_runstate = RunState::PlayerTurn; + let is_ranged = self.ecs.read_storage::(); + let ranged_item = is_ranged.get(item_entity); + if let Some(ranged_item) = ranged_item { + let is_aoe = self.ecs.read_storage::(); + let aoe_item = is_aoe.get(item_entity); + if let Some(aoe_item) = aoe_item { + new_runstate = RunState::ShowTargeting { + range: ranged_item.range, + item: item_entity, + aoe: aoe_item.radius, + } + } else { + new_runstate = + RunState::ShowTargeting { range: ranged_item.range, item: item_entity, aoe: 0 } + } + } else { + let mut intent = self.ecs.write_storage::(); + intent + .insert(*self.ecs.fetch::(), WantsToUseItem { item: item_entity, target: None }) + .expect("Unable to insert intent."); + new_runstate = RunState::PlayerTurn; + } } } } @@ -162,6 +180,20 @@ impl GameState for State { } } } + RunState::ShowTargeting { range, item, aoe } => { + let result = gui::ranged_target(self, ctx, range, aoe); + match result.0 { + gui::ItemMenuResult::Cancel => new_runstate = 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."); + new_runstate = RunState::PlayerTurn; + } + } + } RunState::MainMenu { .. } => { let result = gui::main_menu(self, ctx); match result { @@ -225,6 +257,9 @@ 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::(); @@ -245,7 +280,9 @@ fn main() -> rltk::BError { gs.ecs.insert(map); gs.ecs.insert(Point::new(player_x, player_y)); gs.ecs.insert(player_entity); - gs.ecs.insert(gamelog::GameLog { entries: vec!["Here's your welcome message.".to_string()] }); + gs.ecs.insert(gamelog::GameLog { + entries: vec!["".to_string()], + }); gs.ecs.insert(RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }); gs.ecs.insert(particle_system::ParticleBuilder::new()); gs.ecs.insert(rex_assets::RexAssets::new()); diff --git a/src/particle_system.rs b/src/particle_system.rs index 4be5e77..6928222 100644 --- a/src/particle_system.rs +++ b/src/particle_system.rs @@ -4,6 +4,11 @@ use specs::prelude::*; pub const DEFAULT_PARTICLE_LIFETIME: f32 = 150.0; +/// Runs each tick, deleting particles who are past their expiry. +// Should make an addition to this to also spawn delayed particles, +// running through a list and removing the frame_time_ms from the +// delay. When delay is <= 0, make a particle_builder.request for +// the particle. pub fn cull_dead_particles(ecs: &mut World, ctx: &Rltk) { let mut dead_particles: Vec = Vec::new(); { @@ -41,9 +46,28 @@ impl ParticleBuilder { ParticleBuilder { requests: Vec::new() } } + /// Makes a single particle request. pub fn request(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) { self.requests.push(ParticleRequest { x, y, fg, bg, glyph, lifetime }); } + + // Makes a particle request in the shape of an 'x'. Sort of. + pub fn request_star(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) { + self.request(x, y, fg, bg, glyph, lifetime * 2.0); + self.request(x + 1, y + 1, fg, bg, rltk::to_cp437('/'), lifetime); + self.request(x + 1, y - 1, fg, bg, rltk::to_cp437('\\'), lifetime); + self.request(x - 1, y + 1, fg, bg, rltk::to_cp437('\\'), lifetime); + self.request(x - 1, y - 1, fg, bg, rltk::to_cp437('/'), lifetime); + } + + /// Makes a particle request in the shape of a +. + pub fn request_plus(&mut self, x: i32, y: i32, fg: RGB, bg: RGB, glyph: rltk::FontCharType, lifetime: f32) { + self.request(x, y, fg, bg, glyph, lifetime * 2.0); + self.request(x + 1, y, fg, bg, rltk::to_cp437('─'), lifetime); + self.request(x - 1, y, fg, bg, rltk::to_cp437('─'), lifetime); + self.request(x, y + 1, fg, bg, rltk::to_cp437('│'), lifetime); + self.request(x, y - 1, fg, bg, rltk::to_cp437('│'), lifetime); + } } pub struct ParticleSpawnSystem {} diff --git a/src/spawner.rs b/src/spawner.rs index 40f6cf2..b801464 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,6 +1,6 @@ use super::{ - BlocksTile, CombatStats, Consumable, Item, Monster, Name, Player, Position, ProvidesHealing, Rect, Renderable, - Viewshed, MAPWIDTH, + BlocksTile, CombatStats, Consumable, InflictsDamage, Item, Monster, Name, Player, Position, ProvidesHealing, + Ranged, Rect, Renderable, Viewshed, AOE, MAPWIDTH, }; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; @@ -45,12 +45,14 @@ pub fn random_item(ecs: &mut World, x: i32, y: i32) { match roll { 1 => health_potion(ecs, x, y), 2 => poison_potion(ecs, x, y), + 3 => magic_missile_scroll(ecs, x, y), + 4 => fireball_scroll(ecs, x, y), _ => weak_health_potion(ecs, x, y), } } const MAX_MONSTERS: i32 = 4; -const MAX_ITEMS: i32 = 2; +const MAX_ITEMS: i32 = 6; fn monster(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharType, name: S) { ecs.create_entity() @@ -138,7 +140,7 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) { .with(Name { name: "potion of health".to_string() }) .with(Item {}) .with(Consumable {}) - .with(ProvidesHealing { heal_amount: 12 }) + .with(ProvidesHealing { amount: 12 }) .build(); } @@ -154,7 +156,7 @@ fn weak_health_potion(ecs: &mut World, x: i32, y: i32) { .with(Name { name: "potion of lesser health".to_string() }) .with(Item {}) .with(Consumable {}) - .with(ProvidesHealing { heal_amount: 6 }) + .with(ProvidesHealing { amount: 6 }) .build(); } @@ -170,6 +172,41 @@ fn poison_potion(ecs: &mut World, x: i32, y: i32) { .with(Name { name: "potion of ... health?".to_string() }) .with(Item {}) .with(Consumable {}) - .with(ProvidesHealing { heal_amount: -12 }) + .with(ProvidesHealing { amount: -12 }) + .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::BLUE), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { name: "scroll of magic missile".to_string() }) + .with(Item {}) + .with(Consumable {}) + .with(Ranged { range: 12 }) + .with(InflictsDamage { amount: 10 }) + .build(); +} + +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: "scroll of fireball".to_string() }) + .with(Item {}) + .with(Consumable {}) + .with(Ranged { range: 10 }) + .with(InflictsDamage { amount: 20 }) + .with(AOE { radius: 3 }) .build(); }