From 9adee7a4dcbb81be7f1b7a0e96171a8ba118fbe6 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Mon, 31 Jan 2022 11:25:36 -0500 Subject: [PATCH] Add RangedCombatSystem --- src/camera.rs | 26 +++- src/components.rs | 5 + src/gui.rs | 2 +- src/main.rs | 2 + src/player.rs | 97 +++++++++++++- src/ranged_combat_system.rs | 257 ++++++++++++++++++++++++++++++++++++ src/saveload_system.rs | 2 + src/state.rs | 111 ++++++---------- 8 files changed, 421 insertions(+), 81 deletions(-) create mode 100644 src/ranged_combat_system.rs diff --git a/src/camera.rs b/src/camera.rs index 54b6d72..78f3904 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -2,7 +2,7 @@ use ::rltk::{Point, Rltk}; use ::specs::prelude::*; -use crate::components::{Hidden, Position, Renderable, TileSize}; +use crate::components::{Hidden, Position, Renderable, Target, TileSize}; use crate::map::tile_glyph; use crate::{colors, Map}; @@ -61,6 +61,7 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { let map = ecs.fetch::(); let sizes = ecs.read_storage::(); let entities = ecs.entities(); + let targets = ecs.read_storage::(); let mut data = (&positions, &renderables, &entities, !&hidden) .join() @@ -103,8 +104,8 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { && entity_screen_y < map_height { ctx.set( - entity_screen_x, - entity_screen_y, + entity_screen_x + 1, + entity_screen_y + 1, render.fg, render.bg, render.glyph, @@ -112,6 +113,25 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { } } } + + if targets.get(*entity).is_some() { + let entity_screen_x = pos.x - min_x; + let entity_screen_y = pos.y - min_y; + ctx.set( + entity_screen_x, + entity_screen_y + 1, + colors::RED, + colors::YELLOW, + ::rltk::to_cp437('['), + ); + ctx.set( + entity_screen_x + 2, + entity_screen_y + 1, + colors::RED, + colors::YELLOW, + ::rltk::to_cp437(']'), + ) + } } } diff --git a/src/components.rs b/src/components.rs index 0984f4d..62138e2 100644 --- a/src/components.rs +++ b/src/components.rs @@ -527,3 +527,8 @@ pub struct TileSize { pub struct OnDeath { pub abilities: Vec, } + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToShoot { + pub target: Entity, +} diff --git a/src/gui.rs b/src/gui.rs index 6b70fb7..0262dfb 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -237,7 +237,7 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { }; if let Some(range) = weapon.range { - weapon_info += &format!(" (range: {}, F to fire)", range); + weapon_info += &format!(" (range: {}, F to fire, V cycle targets)", range); } weapon_info += " ├"; ctx.print_color(3, 45, colors::YELLOW, colors::BLACK, &weapon_info); diff --git a/src/main.rs b/src/main.rs index b1eb068..4e5a7d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ mod movement_system; mod particle_system; mod player; mod random_table; +mod ranged_combat_system; mod raws; mod rect; mod rex_assets; @@ -158,6 +159,7 @@ fn init_state() -> State { WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToShoot, WantsToUseItem, Weapon, Wearable, diff --git a/src/player.rs b/src/player.rs index ec9b915..6036286 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,15 +1,16 @@ use std::cmp::{max, min}; -use ::rltk::{Point, Rltk, VirtualKeyCode}; +use ::rltk::{DistanceAlg, Point, Rltk, VirtualKeyCode}; use ::specs::prelude::*; use crate::components::{ - Attributes, BlocksTile, BlocksVisibility, Door, EntityMoved, Faction, HungerClock, HungerState, - Item, Player, Pools, Position, Renderable, Vendor, Viewshed, WantsToMelee, WantsToPickupItem, + Attributes, BlocksTile, BlocksVisibility, Door, EntityMoved, Equipped, Faction, HungerClock, + HungerState, Item, Player, Pools, Position, Renderable, Target, Vendor, Viewshed, WantsToMelee, + WantsToPickupItem, }; use crate::game_log::GameLog; use crate::raws::{self, Reaction, RAWS}; -use crate::{spatial, Map, RunState, State, TileType, VendorMode, WantsToCastSpell}; +use crate::{spatial, Map, RunState, State, TileType, VendorMode, WantsToCastSpell, Weapon}; pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); @@ -347,6 +348,87 @@ fn use_spell_hotkey(gs: &mut State, key: i32) -> RunState { RunState::Ticking } +fn get_player_target_list(ecs: &mut World) -> Vec<(f32, Entity)> { + let mut possible_targets = Vec::new(); + let viewsheds = ecs.read_storage::(); + let player_entity = ecs.fetch::(); + let equipped = ecs.read_storage::(); + let weapon = ecs.read_storage::(); + let map = ecs.fetch::(); + let positions = ecs.read_storage::(); + let factions = ecs.read_storage::(); + + for (equipped, weapon) in (&equipped, &weapon).join() { + if equipped.owner == *player_entity { + if let Some(range) = weapon.range { + if let Some(vs) = viewsheds.get(*player_entity) { + let player_pos = positions.get(*player_entity).unwrap(); + + for tile_point in vs.visible_tiles.iter() { + let tile_idx = map.xy_idx(tile_point.x, tile_point.y); + let distance_to_target = DistanceAlg::Pythagoras + .distance2d(*tile_point, Point::from(*player_pos)); + if distance_to_target < range as f32 { + spatial::for_each_tile_content(tile_idx, |possible_target| { + if possible_target != *player_entity + && factions.get(possible_target).is_some() + { + possible_targets.push((distance_to_target, possible_target)); + } + }); + } + } + } + } + } + } + + possible_targets.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + + possible_targets +} + +fn cycle_target(ecs: &mut World) { + let possible_targets = get_player_target_list(ecs); + let mut targets = ecs.write_storage::(); + let entities = ecs.entities(); + let mut current_target: Option = None; + + for (e, _t) in (&entities, &targets).join() { + current_target = Some(e); + } + + targets.clear(); + if let Some(current_target) = current_target { + if !possible_targets.len() > 1 { + let mut index = 0; + for (i, target) in possible_targets.iter().enumerate() { + if target.1 == current_target { + index = 1; + } + } + + if index > possible_targets.len() - 2 { + targets.insert(possible_targets[0].1, Target {}); + } else { + targets.insert(possible_targets[index + 1].1, Target {}); + } + } + } +} + +pub fn end_turn_targeting(ecs: &mut World) { + let possible_targets = get_player_target_list(ecs); + let mut targets = ecs.write_storage::(); + targets.clear(); + + if !possible_targets.is_empty() { + targets + .insert(possible_targets[0].1, Target {}) + .expect("Failed to insert Target tag"); + } +} + pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { // Hotkeys if (ctx.shift || ctx.control) && ctx.key.is_some() { @@ -453,6 +535,13 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { // Cheating! VirtualKeyCode::Backslash => return RunState::ShowCheatMenu, + // Ranged + VirtualKeyCode::V => { + cycle_target(&mut gs.ecs); + + return RunState::AwaitingInput; + } + _ => return RunState::AwaitingInput, }, } diff --git a/src/ranged_combat_system.rs b/src/ranged_combat_system.rs new file mode 100644 index 0000000..7f0a5ed --- /dev/null +++ b/src/ranged_combat_system.rs @@ -0,0 +1,257 @@ +use ::specs::prelude::*; +use rltk::{to_cp437, LineAlg, Point, RandomNumberGenerator}; + +use crate::components::{ + Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, Name, NaturalAttackDefense, + Pools, Position, Skill, Skills, WantsToShoot, Weapon, WeaponAttribute, Wearable, +}; +use crate::effects::{add_effect, EffectType, Targets}; +use crate::game_log::GameLog; +use crate::gamesystem::skill_bonus; +use crate::{colors, Map}; + +pub struct RangedCombatSystem {} + +impl<'a> System<'a> for RangedCombatSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteExpect<'a, GameLog>, + WriteStorage<'a, WantsToShoot>, + ReadStorage<'a, Name>, + ReadStorage<'a, Attributes>, + ReadStorage<'a, Skills>, + ReadStorage<'a, HungerClock>, + ReadStorage<'a, Pools>, + WriteExpect<'a, RandomNumberGenerator>, + ReadStorage<'a, Equipped>, + ReadStorage<'a, Weapon>, + ReadStorage<'a, Wearable>, + ReadStorage<'a, NaturalAttackDefense>, + ReadStorage<'a, Position>, + ReadExpect<'a, Map>, + ); + + fn run(&mut self, data: Self::SystemData) { + let ( + entities, + mut log, + mut wants_shoot, + names, + attributes, + skills, + hunger_clock, + pools, + mut rng, + equipped_items, + weapons, + wearables, + natural, + positions, + map, + ) = data; + + for (entity, wants_shoot, name, attacker_attributes, attacker_skills, attacker_pools) in ( + &entities, + &wants_shoot, + &names, + &attributes, + &skills, + &pools, + ) + .join() + { + // Are the attacker and defender alive? Only attack if they are + let target_pools = pools.get(wants_shoot.target).unwrap(); + let target_attributes = attributes.get(wants_shoot.target).unwrap(); + let target_skills = skills.get(wants_shoot.target).unwrap(); + if attacker_pools.hit_points.current > 0 && target_pools.hit_points.current > 0 { + let target_name = names.get(wants_shoot.target).unwrap(); + + // Fire projectile effect + // let apos = positions.get(entity).unwrap(); + // let dpos = positions.get(wants_shoot.target).unwrap(); + // add_effect( + // None, + // EffectType::ParticleProjectile { + // glyph: to_cp437('*'), + // fg: colors::CYAN, + // bg: colors::BLACK, + // lifespan: 300.0, + // speed: 50.0, + // path: ::rltk::line2d( + // LineAlg::Bresenham, + // Point::from(*apos), + // Point::from(*dpos), + // ), + // }, + // Targets::Tile { + // tile_idx: map.xy_idx(apos.x, apos.y) as i32, + // }, + // ); + + // Define the basic unarmed attack -- overridden by wielding check below if a weapon is equipped + let mut weapon_info = Weapon { + range: None, + attribute: WeaponAttribute::Might, + hit_bonus: 0, + damage_n_dice: 1, + damage_die_type: 4, + damage_bonus: 0, + proc_chance: None, + proc_target: None, + }; + + if let Some(nat) = natural.get(entity) { + if !nat.attacks.is_empty() { + let attack_index = if nat.attacks.len() == 1 { + 0 + } else { + rng.roll_dice(1, nat.attacks.len() as i32) as usize - 1 + }; + + let attk = &nat.attacks[attack_index]; + + weapon_info.hit_bonus = attk.hit_bonus; + weapon_info.damage_n_dice = attk.damage_n_dice; + weapon_info.damage_die_type = attk.damage_die_type; + weapon_info.damage_bonus = attk.damage_bonus; + } + } + + let mut weapon_entity: Option = None; + for (weaponentity, wielded, weapon) in (&entities, &equipped_items, &weapons).join() + { + if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { + weapon_info = weapon.clone(); + weapon_entity = Some(weaponentity); + } + } + + let natural_roll = rng.roll_dice(1, 20); + let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might { + attacker_attributes.might.bonus + } else { + attacker_attributes.quickness.bonus + }; + let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_hit_bonus = weapon_info.hit_bonus; + let mut status_hit_bonus = 0; + if let Some(hc) = hunger_clock.get(entity) { + // Well-Fed grants +1 + if hc.state == HungerState::WellFed { + status_hit_bonus += 1; + } + } + let modified_hit_roll = natural_roll + + attribute_hit_bonus + + skill_hit_bonus + + weapon_hit_bonus + + status_hit_bonus; + + let mut armor_item_bonus_f = 0.0; + for (wielded, armor) in (&equipped_items, &wearables).join() { + if wielded.owner == wants_shoot.target { + armor_item_bonus_f += armor.armor_class; + } + } + + let base_armor_class = match natural.get(wants_shoot.target) { + None => 10, + Some(nat) => nat.armor_class.unwrap_or(10), + }; + let armor_quickness_bonus = target_attributes.quickness.bonus; + let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills); + let armor_item_bonus = armor_item_bonus_f as i32; + let armor_class = + base_armor_class + armor_quickness_bonus + armor_skill_bonus + armor_item_bonus; + + if natural_roll != 1 && (natural_roll == 20 || modified_hit_roll > armor_class) { + // Target hit! Until we support weapons, we're going with 1d4 + let base_damage = rng.roll_dice(1, 4); + let attr_damage_bonus = attacker_attributes.might.bonus; + let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_damage_bonus = weapon_info.damage_bonus; + + let damage = i32::max( + 0, + base_damage + + attr_damage_bonus + + skill_hit_bonus + + skill_damage_bonus + + weapon_damage_bonus, + ); + add_effect( + Some(entity), + EffectType::Damage { amount: damage }, + Targets::Single { + target: wants_shoot.target, + }, + ); + log.append(format!( + "{} hits {} for {} hp.", + &name.name, &target_name.name, damage + )); + + // Proc effects + if let Some(chance) = &weapon_info.proc_chance { + if rng.roll_dice(1, 100) <= (chance * 100.0) as i32 { + let effect_target = if weapon_info.proc_target.unwrap() == "Self" { + Targets::Single { target: entity } + } else { + Targets::Single { + target: wants_shoot.target, + } + }; + add_effect( + Some(entity), + EffectType::ItemUse { + item: weapon_entity.unwrap(), + }, + effect_target, + ); + } + } + } else if natural_roll == 1 { + // Natural 1 miss + log.append(format!( + "{} considers attacking {}, but misjudges the timing.", + name.name, target_name.name + )); + add_effect( + None, + EffectType::Particle { + glyph: rltk::to_cp437('‼'), + fg: colors::BLUE, + bg: colors::BLACK, + lifespan: 200.0, + }, + Targets::Single { + target: wants_shoot.target, + }, + ); + } else { + // Miss + log.append(format!( + "{} attacks {}, but can't connect", + name.name, target_name.name + )); + add_effect( + None, + EffectType::Particle { + glyph: rltk::to_cp437('‼'), + fg: colors::CYAN, + bg: colors::BLACK, + lifespan: 200.0, + }, + Targets::Single { + target: wants_shoot.target, + }, + ); + } + } + } + + wants_shoot.clear(); + } +} diff --git a/src/saveload_system.rs b/src/saveload_system.rs index c4402f3..f08055c 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -136,6 +136,7 @@ pub fn save_game(ecs: &mut World) { WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToShoot, WantsToUseItem, Weapon, Wearable, @@ -271,6 +272,7 @@ pub fn load_game(ecs: &mut World) { WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToShoot, WantsToUseItem, Weapon, Wearable, diff --git a/src/state.rs b/src/state.rs index e285968..4291a95 100644 --- a/src/state.rs +++ b/src/state.rs @@ -17,10 +17,11 @@ use crate::melee_combat_system::MeleeCombatSystem; use crate::movement_system::MovementSystem; use crate::particle_system::{self, ParticleSpawnSystem}; use crate::player::*; +use crate::ranged_combat_system::RangedCombatSystem; use crate::raws::*; use crate::trigger_system::TriggerSystem; use crate::visibility_system::VisibilitySystem; -use crate::{ai, camera, damage_system, effects, saveload_system, spawner}; +use crate::{ai, camera, damage_system, effects, player, saveload_system, spawner}; /// Whether to show a visual representation of map generation pub const SHOW_MAPGEN_VISUALIZER: bool = false; @@ -79,82 +80,37 @@ impl State { } fn run_systems(&mut self) { - let mut mapindex = MapIndexingSystem {}; - mapindex.run_now(&self.ecs); + MapIndexingSystem {}.run_now(&self.ecs); + VisibilitySystem {}.run_now(&self.ecs); - let mut vis = VisibilitySystem {}; - vis.run_now(&self.ecs); + ai::EncumbranceSystem {}.run_now(&self.ecs); + ai::InitiativeSystem {}.run_now(&self.ecs); + ai::TurnStatusSystem {}.run_now(&self.ecs); + ai::QuipSystem {}.run_now(&self.ecs); + ai::AdjacentAI {}.run_now(&self.ecs); + ai::VisibleAI {}.run_now(&self.ecs); + ai::ApproachAI {}.run_now(&self.ecs); + ai::FleeAI {}.run_now(&self.ecs); + ai::ChaseAI {}.run_now(&self.ecs); + ai::DefaultMoveAI {}.run_now(&self.ecs); - let mut encumbrance = ai::EncumbranceSystem {}; - encumbrance.run_now(&self.ecs); - - let mut initiative = ai::InitiativeSystem {}; - initiative.run_now(&self.ecs); - - let mut turnstatus = ai::TurnStatusSystem {}; - turnstatus.run_now(&self.ecs); - - let mut quipper = ai::QuipSystem {}; - quipper.run_now(&self.ecs); - - let mut adjacent = ai::AdjacentAI {}; - adjacent.run_now(&self.ecs); - - let mut visible = ai::VisibleAI {}; - visible.run_now(&self.ecs); - - let mut approach = ai::ApproachAI {}; - approach.run_now(&self.ecs); - - let mut flee = ai::FleeAI {}; - flee.run_now(&self.ecs); - - let mut chase = ai::ChaseAI {}; - chase.run_now(&self.ecs); - - let mut defaultmove = ai::DefaultMoveAI {}; - defaultmove.run_now(&self.ecs); - - let mut moving = MovementSystem {}; - moving.run_now(&self.ecs); - - let mut triggers = TriggerSystem {}; - triggers.run_now(&self.ecs); - - let mut melee = MeleeCombatSystem {}; - melee.run_now(&self.ecs); - - let mut pickup = ItemCollectionSystem {}; - pickup.run_now(&self.ecs); - - let mut itemequip = ItemEquipOnUse {}; - itemequip.run_now(&self.ecs); - - let mut itemuse = ItemUseSystem {}; - itemuse.run_now(&self.ecs); - - let mut spelluse = SpellUseSystem {}; - spelluse.run_now(&self.ecs); - - let mut item_id = ItemIdentificationSystem {}; - item_id.run_now(&self.ecs); - - let mut drop_items = ItemDropSystem {}; - drop_items.run_now(&self.ecs); - - let mut item_remove = ItemRemoveSystem {}; - item_remove.run_now(&self.ecs); - - let mut hunger = HungerSystem {}; - hunger.run_now(&self.ecs); + MovementSystem {}.run_now(&self.ecs); + TriggerSystem {}.run_now(&self.ecs); + MeleeCombatSystem {}.run_now(&self.ecs); + RangedCombatSystem {}.run_now(&self.ecs); + ItemCollectionSystem {}.run_now(&self.ecs); + ItemEquipOnUse {}.run_now(&self.ecs); + ItemUseSystem {}.run_now(&self.ecs); + SpellUseSystem {}.run_now(&self.ecs); + ItemIdentificationSystem {}.run_now(&self.ecs); + ItemDropSystem {}.run_now(&self.ecs); + ItemRemoveSystem {}.run_now(&self.ecs); + HungerSystem {}.run_now(&self.ecs); effects::run_effects_queue(&mut self.ecs); - let mut particles = ParticleSpawnSystem {}; - particles.run_now(&self.ecs); - - let mut lighting = LightingSystem {}; - lighting.run_now(&self.ecs); + ParticleSpawnSystem {}.run_now(&self.ecs); + LightingSystem {}.run_now(&self.ecs); self.ecs.maintain(); } @@ -259,12 +215,17 @@ impl GameState for State { newrunstate = player_input(self, ctx); } RunState::Ticking => { + let mut should_change_target = false; while newrunstate == RunState::Ticking { self.run_systems(); self.ecs.maintain(); newrunstate = match *self.ecs.fetch::() { - RunState::AwaitingInput => RunState::AwaitingInput, + RunState::AwaitingInput => { + should_change_target = true; + + RunState::AwaitingInput + } RunState::MagicMapReveal { .. } => RunState::MagicMapReveal { row: 0 }, RunState::TownPortal => RunState::TownPortal, RunState::TeleportingToOtherLevel { x, y, depth } => { @@ -275,6 +236,10 @@ impl GameState for State { _ => RunState::Ticking, }; } + + if should_change_target { + player::end_turn_targeting(&mut self.ecs); + } } RunState::ShowInventory => { let result = gui::show_inventory(self, ctx);