diff --git a/src/ai/adjacent_ai_system.rs b/src/ai/adjacent_ai_system.rs index 6014ade..0312c8d 100644 --- a/src/ai/adjacent_ai_system.rs +++ b/src/ai/adjacent_ai_system.rs @@ -16,59 +16,56 @@ impl<'a> System<'a> for AdjacentAI { ); fn run(&mut self, data: Self::SystemData) { - let (mut turns, factions, positions, map, mut wants_to_melee, entities, player) = data; + let (mut turns, factions, positions, map, mut want_melee, entities, player) = data; let mut turn_done: Vec = Vec::new(); - for (entity, _turn, faction, pos) in (&entities, &turns, &factions, &positions).join() { - if entity == *player { - continue; - } - let mut reactions: Vec<(Entity, Reaction)> = Vec::new(); - let idx = map.xy_idx(pos.x, pos.y); - let (w, h) = (map.width, map.height); - // Evaluate adjacent squares, add possible reactions - let mut eval_idx: usize = idx; - if pos.x > 0 { - eval_idx = idx - 1; - } - if pos.x < w - 1 { - eval_idx = idx + 1; - } - if pos.y > 0 { - eval_idx = idx - w as usize; - } - if pos.y < h - 1 { - eval_idx = idx + w as usize; - } - if pos.y > 0 && pos.x > 0 { - eval_idx = (idx - w as usize) - 1; - } - if pos.y > 0 && pos.x < w - 1 { - eval_idx = (idx - w as usize) + 1; - } - if pos.y < h - 1 && pos.x > 0 { - eval_idx = (idx + w as usize) - 1; - } - if pos.y < h - 1 && pos.x < w - 1 { - eval_idx = (idx + w as usize) + 1; - } - if eval_idx != idx { - evaluate(eval_idx, &factions, &faction.name, &mut reactions); - } - let mut done = false; - for reaction in reactions.iter() { - if let Reaction::Attack = reaction.1 { - wants_to_melee - .insert(entity, WantsToMelee { target: reaction.0 }) - .expect("Error inserting WantsToMelee"); - done = true; + for (entity, _turn, my_faction, pos) in (&entities, &turns, &factions, &positions).join() { + if entity != *player { + let mut reactions: Vec<(Entity, Reaction)> = Vec::new(); + let idx = map.xy_idx(pos.x, pos.y); + let w = map.width; + let h = map.height; + // Add possible reactions to adjacents for each direction + if pos.x > 0 { + evaluate(idx - 1, &factions, &my_faction.name, &mut reactions); + } + if pos.x < w - 1 { + evaluate(idx + 1, &factions, &my_faction.name, &mut reactions); + } + if pos.y > 0 { + evaluate(idx - w as usize, &factions, &my_faction.name, &mut reactions); + } + if pos.y < h - 1 { + evaluate(idx + w as usize, &factions, &my_faction.name, &mut reactions); + } + if pos.y > 0 && pos.x > 0 { + evaluate((idx - w as usize) - 1, &factions, &my_faction.name, &mut reactions); + } + if pos.y > 0 && pos.x < w - 1 { + evaluate((idx - w as usize) + 1, &factions, &my_faction.name, &mut reactions); + } + if pos.y < h - 1 && pos.x > 0 { + evaluate((idx + w as usize) - 1, &factions, &my_faction.name, &mut reactions); + } + if pos.y < h - 1 && pos.x < w - 1 { + evaluate((idx + w as usize) + 1, &factions, &my_faction.name, &mut reactions); + } + + let mut done = false; + for reaction in reactions.iter() { + if let Reaction::Attack = reaction.1 { + want_melee.insert(entity, WantsToMelee { target: reaction.0 }).expect("Error inserting melee"); + done = true; + } + } + + if done { + turn_done.push(entity); } } - if done { - turn_done.push(entity); - } } - // Remove turn from entities that are done + + // Remove turn marker for those that are done for done in turn_done.iter() { turns.remove(*done); } diff --git a/src/damage_system.rs b/src/damage_system.rs index e39ee88..6632bcc 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -2,7 +2,7 @@ use super::{ gamelog, Attributes, Equipped, GrantsXP, InBackpack, Item, LootTable, Map, Name, ParticleBuilder, Player, Pools, Position, RunState, SufferDamage, }; -use crate::gamesystem::{mana_per_level, player_hp_per_level}; +use crate::gamesystem::{hp_per_level, mana_per_level}; use rltk::prelude::*; use specs::prelude::*; @@ -98,7 +98,7 @@ impl<'a> System<'a> for DamageSystem { } } // Roll for HP gain this level - let hp_gained = player_hp_per_level( + let hp_gained = hp_per_level( &mut rng, player_attributes.constitution.base + player_attributes.constitution.modifiers, ); diff --git a/src/effects/damage.rs b/src/effects/damage.rs new file mode 100644 index 0000000..bdcc9c4 --- /dev/null +++ b/src/effects/damage.rs @@ -0,0 +1,180 @@ +use super::{add_effect, targeting, EffectSpawner, EffectType, Entity, Targets, World}; +use crate::{ + gamelog, + gamesystem::{hp_per_level, mana_per_level}, + Attributes, GrantsXP, Map, Player, Pools, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME, +}; +use rltk::prelude::*; +use specs::prelude::*; + +pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { + let mut pools = ecs.write_storage::(); + if let Some(target_pool) = pools.get_mut(target) { + if !target_pool.god { + if let EffectType::Damage { amount } = damage.effect_type { + target_pool.hit_points.current -= amount; + add_effect(None, EffectType::Bloodstain, Targets::Entity { target }); + add_effect( + None, + EffectType::Particle { + glyph: to_cp437('‼'), + fg: RGB::named(ORANGE), + bg: RGB::named(BLACK), + lifespan: DEFAULT_PARTICLE_LIFETIME, + delay: 0.0, + }, + Targets::Entity { target }, + ); + if target_pool.hit_points.current < 1 { + add_effect(damage.source, EffectType::EntityDeath, Targets::Entity { target }); + } + } + } + } +} + +pub fn bloodstain(ecs: &mut World, target: usize) { + let mut map = ecs.fetch_mut::(); + // If the current tile isn't bloody, bloody it. + if !map.bloodstains.contains(&target) { + map.bloodstains.insert(target); + return; + } + let mut spread: i32 = target as i32; + let mut attempts: i32 = 0; + // Otherwise, roll to move one tile in any direction. + // If this tile isn't bloody, bloody it. If not, loop. + loop { + let mut rng = ecs.write_resource::(); + attempts += 1; + spread = match rng.roll_dice(1, 8) { + 1 => spread + 1, + 2 => spread - 1, + 3 => spread + 1 + map.width, + 4 => spread - 1 + map.width, + 5 => spread + 1 - map.width, + 6 => spread - 1 - map.width, + 7 => spread + map.width, + _ => spread - map.width, + }; + // - If we're in bounds and the tile is unbloodied, bloody it and return. + // - If we ever leave bounds, return. + // - Roll a dice on each failed attempt, with an increasing change to return (soft-capping max spread) + if spread > 0 && spread < (map.height * map.width) { + if !map.bloodstains.contains(&(spread as usize)) { + map.bloodstains.insert(spread as usize); + return; + } + if rng.roll_dice(1, 10 - attempts) == 1 { + return; + } + } else { + return; + } + } +} + +pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + let mut xp_gain = 0; + let mut pools = ecs.write_storage::(); + let attributes = ecs.read_storage::(); + console::log("HERE"); + + // If the target has a position, remove it from the SpatialMap. + if let Some(pos) = targeting::entity_position(ecs, target) { + console::log("HEREE"); + crate::spatial::remove_entity(target, pos as usize); + console::log("HEREEE"); + } + // If the target was killed by a source, cont. + if let Some(source) = effect.source { + // Calc XP value of target. + if let Some(xp_value) = ecs.read_storage::().get(target) { + xp_gain += xp_value.amount; + } + // If there was XP, run through XP-gain and level-up. + if xp_gain != 0 { + let mut source_pools = pools.get_mut(source).unwrap(); + let source_attributes = attributes.get(source).unwrap(); + source_pools.xp += xp_gain; + let mut next_level_requirement = -1; + if source_pools.level < 10 { + next_level_requirement = 20 * 2_i32.pow(source_pools.level as u32 - 1); + } else if source_pools.level < 20 { + next_level_requirement = 10000 * 2_i32.pow(source_pools.level as u32 - 10); + } else if source_pools.level < 30 { + next_level_requirement = 10000000 * (source_pools.level - 19); + } + if next_level_requirement != -1 && source_pools.xp >= next_level_requirement { + source_pools.level += 1; + // If it was the PLAYER that levelled up: + if ecs.read_storage::().get(source).is_some() { + gamelog::record_event("player_level", 1); + gamelog::Logger::new() + .append("Welcome to experience level") + .append(source_pools.level) + .append(".") + .log(); + let player_pos = ecs.fetch::(); + let map = ecs.fetch_mut::(); + for i in 0..5 { + if player_pos.y - i > 1 { + add_effect( + None, + EffectType::Particle { + glyph: to_cp437('░'), + fg: RGB::named(GOLD), + bg: RGB::named(BLACK), + lifespan: LONG_PARTICLE_LIFETIME, + delay: i as f32 * 100.0, + }, + Targets::Tile { target: map.xy_idx(player_pos.x, player_pos.y - i) }, + ); + if i > 2 { + add_effect( + None, + EffectType::Particle { + glyph: to_cp437('░'), + fg: RGB::named(GOLD), + bg: RGB::named(BLACK), + lifespan: LONG_PARTICLE_LIFETIME, + delay: i as f32 * 100.0, + }, + Targets::Tile { target: map.xy_idx(player_pos.x + (i - 2), player_pos.y - i) }, + ); + add_effect( + None, + EffectType::Particle { + glyph: to_cp437('░'), + fg: RGB::named(GOLD), + bg: RGB::named(BLACK), + lifespan: LONG_PARTICLE_LIFETIME, + delay: i as f32 * 100.0, + }, + Targets::Tile { target: map.xy_idx(player_pos.x - (i - 2), player_pos.y - i) }, + ); + } + } + } + } else { + console::log("DEBUGINFO: Something other than the player levelled up."); + // TODO: Growing up, NPC-specific level-up cases. + } + let mut rng = ecs.write_resource::(); + let hp_gained = hp_per_level( + &mut rng, + source_attributes.constitution.base + source_attributes.constitution.modifiers, + ); + let mana_gained = mana_per_level( + &mut rng, + source_attributes.intelligence.base + source_attributes.intelligence.modifiers, + ); + source_pools.hit_points.max += hp_gained; + source_pools.hit_points.current += hp_gained; + // Roll for MANA gain this level + source_pools.mana.max += mana_gained; + source_pools.mana.current += mana_gained; + } + } + } +} diff --git a/src/effects/mod.rs b/src/effects/mod.rs new file mode 100644 index 0000000..88f5f7f --- /dev/null +++ b/src/effects/mod.rs @@ -0,0 +1,105 @@ +use crate::spatial; +use rltk::prelude::*; +use specs::prelude::*; +use std::collections::VecDeque; +use std::sync::Mutex; + +mod damage; +mod particles; +mod targeting; + +lazy_static! { + pub static ref EFFECT_QUEUE: Mutex> = Mutex::new(VecDeque::new()); +} + +pub enum EffectType { + Damage { amount: i32 }, + Bloodstain, + Particle { glyph: FontCharType, fg: RGB, bg: RGB, lifespan: f32, delay: f32 }, + EntityDeath, +} + +#[derive(Clone)] +pub enum Targets { + Entity { target: Entity }, + EntityList { targets: Vec }, + Tile { target: usize }, + TileList { targets: Vec }, +} + +pub struct EffectSpawner { + pub source: Option, + pub effect_type: EffectType, + pub target: Targets, +} + +/// Adds an effect to the effects queue +pub fn add_effect(source: Option, effect_type: EffectType, target: Targets) { + let mut lock = EFFECT_QUEUE.lock().unwrap(); + lock.push_back(EffectSpawner { source, effect_type, target }); +} + +/// Iterates through the effects queue, applying each effect to their target. +pub fn run_effects_queue(ecs: &mut World) { + loop { + let effect: Option = EFFECT_QUEUE.lock().unwrap().pop_front(); + if let Some(effect) = effect { + target_applicator(ecs, &effect); + } else { + break; + } + } +} + +/// Applies an effect to the correct target(s). +fn target_applicator(ecs: &mut World, effect: &EffectSpawner) { + match &effect.target { + Targets::Tile { target } => affect_tile(ecs, effect, *target), + Targets::TileList { targets } => targets.iter().for_each(|target| affect_tile(ecs, effect, *target)), + Targets::Entity { target } => affect_entity(ecs, effect, *target), + Targets::EntityList { targets } => targets.iter().for_each(|target| affect_entity(ecs, effect, *target)), + } +} + +/// Checks if a given effect affects entities or not. +fn tile_effect_hits_entities(effect: &EffectType) -> bool { + match effect { + EffectType::Damage { .. } => true, + _ => false, + } +} + +/// Runs an effect on a given tile index +fn affect_tile(ecs: &mut World, effect: &EffectSpawner, target: usize) { + if tile_effect_hits_entities(&effect.effect_type) { + spatial::for_each_tile_content(target, |entity| { + affect_entity(ecs, effect, entity); + }); + } + + match &effect.effect_type { + EffectType::Bloodstain => damage::bloodstain(ecs, target), + EffectType::Particle { .. } => particles::particle_to_tile(ecs, target as i32, &effect), + _ => {} + } + // Run the effect +} + +/// Runs an effect on a given entity +fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + match &effect.effect_type { + EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target), + EffectType::Bloodstain { .. } => { + if let Some(pos) = targeting::entity_position(ecs, target) { + damage::bloodstain(ecs, pos) + } + } + EffectType::Particle { .. } => { + if let Some(pos) = targeting::entity_position(ecs, target) { + particles::particle_to_tile(ecs, pos as i32, &effect) + } + } + EffectType::EntityDeath => damage::entity_death(ecs, effect, target), + _ => {} + } +} diff --git a/src/effects/particles.rs b/src/effects/particles.rs new file mode 100644 index 0000000..9e64d9c --- /dev/null +++ b/src/effects/particles.rs @@ -0,0 +1,15 @@ +use super::{EffectSpawner, EffectType}; +use crate::{Map, ParticleBuilder}; +use specs::prelude::*; + +pub fn particle_to_tile(ecs: &mut World, target: i32, effect: &EffectSpawner) { + if let EffectType::Particle { glyph, fg, bg, lifespan, delay } = effect.effect_type { + let map = ecs.fetch::(); + let mut particle_builder = ecs.fetch_mut::(); + if delay <= 0.0 { + particle_builder.request(target % map.width, target / map.width, fg, bg, glyph, lifespan); + } else { + particle_builder.delay(target % map.width, target / map.width, fg, bg, glyph, lifespan, delay); + } + } +} diff --git a/src/effects/targeting.rs b/src/effects/targeting.rs new file mode 100644 index 0000000..1b620ad --- /dev/null +++ b/src/effects/targeting.rs @@ -0,0 +1,10 @@ +use crate::{Map, Position}; +use specs::prelude::*; + +pub fn entity_position(ecs: &World, target: Entity) -> Option { + if let Some(position) = ecs.read_storage::().get(target) { + let map = ecs.fetch::(); + return Some(map.xy_idx(position.x, position.y)); + } + return None; +} diff --git a/src/gamesystem.rs b/src/gamesystem.rs index afd5e06..304e089 100644 --- a/src/gamesystem.rs +++ b/src/gamesystem.rs @@ -4,7 +4,7 @@ pub fn attr_bonus(value: i32) -> i32 { return (value - 10) / 2; } -pub fn player_hp_per_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32) -> i32 { +pub fn hp_per_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32) -> i32 { return rng.roll_dice(1, 8) + attr_bonus(constitution); } @@ -12,12 +12,12 @@ pub fn player_hp_per_level(rng: &mut rltk::RandomNumberGenerator, constitution: pub fn player_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { let mut total = 10 + attr_bonus(constitution); for _i in 0..level { - total += player_hp_per_level(rng, constitution); + total += hp_per_level(rng, constitution); } return total; } -pub fn npc_hp(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { +pub fn npc_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { if level == 0 { return rng.roll_dice(1, 4); } diff --git a/src/main.rs b/src/main.rs index 76aaa12..6f217ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,6 @@ mod saveload_system; mod spawner; mod visibility_system; use visibility_system::VisibilitySystem; -mod map_indexing_system; -use map_indexing_system::MapIndexingSystem; mod damage_system; use damage_system::*; mod hunger_system; @@ -32,6 +30,7 @@ mod inventory; mod particle_system; use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME}; mod ai; +mod effects; mod gamesystem; mod random_table; mod rex_assets; @@ -89,7 +88,7 @@ impl State { } fn run_systems(&mut self) { - let mut mapindex = MapIndexingSystem {}; + let mut mapindex = spatial::MapIndexingSystem {}; let mut vis = VisibilitySystem {}; let mut regen_system = ai::RegenSystem {}; let mut energy = ai::EnergySystem {}; @@ -123,6 +122,7 @@ impl State { item_id_system.run_now(&self.ecs); melee_system.run_now(&self.ecs); damage_system.run_now(&self.ecs); + effects::run_effects_queue(&mut self.ecs); hunger_clock.run_now(&self.ecs); particle_system.run_now(&self.ecs); diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 48c8fd7..32b2473 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,7 +1,7 @@ use super::{ + effects::{add_effect, EffectType, Targets}, gamelog, gamesystem, ArmourClassBonus, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon, - MultiAttack, Name, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, SufferDamage, WantsToMelee, - WeaponAttribute, + MultiAttack, Name, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, WantsToMelee, WeaponAttribute, }; use specs::prelude::*; @@ -16,7 +16,6 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, Attributes>, ReadStorage<'a, Skills>, ReadStorage<'a, Pools>, - WriteStorage<'a, SufferDamage>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, ReadStorage<'a, Equipped>, @@ -37,7 +36,6 @@ impl<'a> System<'a> for MeleeCombatSystem { attributes, skills, pools, - mut inflict_damage, mut particle_builder, positions, equipped, @@ -223,7 +221,11 @@ impl<'a> System<'a> for MeleeCombatSystem { if let Some(pos) = pos { particle_builder.damage_taken(pos.x, pos.y) } - SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage, entity == *player_entity); + add_effect( + Some(entity), + EffectType::Damage { amount: damage }, + Targets::Entity { target: wants_melee.target }, + ); if entity == *player_entity { something_to_log = true; logger = logger // You hit the . diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 2918979..5c5d8bc 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -408,7 +408,7 @@ pub fn spawn_named_mob( // Should really use existing RNG here let mut rng = rltk::RandomNumberGenerator::new(); - let mob_hp = npc_hp(&mut rng, mob_con, mob_level); + let mob_hp = npc_hp_at_level(&mut rng, mob_con, mob_level); let mob_mana = mana_at_level(&mut rng, mob_int, mob_level); let mob_bac = if mob_template.bac.is_some() { mob_template.bac.unwrap() } else { 10 }; diff --git a/src/map_indexing_system.rs b/src/spatial/map_indexing_system.rs similarity index 94% rename from src/map_indexing_system.rs rename to src/spatial/map_indexing_system.rs index d9e42f7..18aab7b 100644 --- a/src/map_indexing_system.rs +++ b/src/spatial/map_indexing_system.rs @@ -1,4 +1,4 @@ -use super::{spatial, BlocksTile, Map, Pools, Position}; +use crate::{spatial, BlocksTile, Map, Pools, Position}; use specs::prelude::*; pub struct MapIndexingSystem {} diff --git a/src/spatial/mod.rs b/src/spatial/mod.rs index daf3d0b..d2d6e83 100644 --- a/src/spatial/mod.rs +++ b/src/spatial/mod.rs @@ -2,6 +2,9 @@ use crate::{tile_walkable, Map, RunState}; use specs::prelude::*; use std::sync::Mutex; +mod map_indexing_system; +pub use map_indexing_system::MapIndexingSystem; + struct SpatialMap { blocked: Vec<(bool, bool)>, tile_content: Vec>, diff --git a/src/trigger_system.rs b/src/trigger_system.rs index 0b0782f..1333847 100644 --- a/src/trigger_system.rs +++ b/src/trigger_system.rs @@ -1,6 +1,7 @@ use super::{ + effects::{add_effect, EffectType, Targets}, gamelog, Confusion, EntityMoved, EntryTrigger, Hidden, InflictsDamage, Map, Name, ParticleBuilder, Position, - SingleActivation, SufferDamage, + SingleActivation, }; use specs::prelude::*; @@ -15,7 +16,6 @@ impl<'a> System<'a> for TriggerSystem { ReadStorage<'a, EntryTrigger>, ReadStorage<'a, InflictsDamage>, WriteStorage<'a, Confusion>, - WriteStorage<'a, SufferDamage>, WriteStorage<'a, Hidden>, ReadStorage<'a, SingleActivation>, ReadStorage<'a, Name>, @@ -32,7 +32,6 @@ impl<'a> System<'a> for TriggerSystem { entry_trigger, inflicts_damage, mut confusion, - mut inflict_damage, mut hidden, single_activation, names, @@ -63,7 +62,11 @@ impl<'a> System<'a> for TriggerSystem { if let Some(damage) = damage { let damage_roll = rng.roll_dice(damage.n_dice, damage.sides) + damage.modifier; particle_builder.damage_taken(pos.x, pos.y); - SufferDamage::new_damage(&mut inflict_damage, entity, damage_roll, false); + add_effect( + None, + EffectType::Damage { amount: damage_roll }, + Targets::Entity { target: entity }, + ); } let confuses = confusion.get(entity_id);