Beginning the conversion to an Effects system

This commit is contained in:
Llywelwyn 2023-08-16 15:11:49 +01:00
parent a6690029e6
commit efe15705ad
13 changed files with 382 additions and 67 deletions

180
src/effects/damage.rs Normal file
View file

@ -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::<Pools>();
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::<Map>();
// 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::<RandomNumberGenerator>();
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::<Pools>();
let attributes = ecs.read_storage::<Attributes>();
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::<GrantsXP>().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::<Player>().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::<Point>();
let map = ecs.fetch_mut::<Map>();
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::<RandomNumberGenerator>();
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;
}
}
}
}

105
src/effects/mod.rs Normal file
View file

@ -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<VecDeque<EffectSpawner>> = 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<Entity> },
Tile { target: usize },
TileList { targets: Vec<usize> },
}
pub struct EffectSpawner {
pub source: Option<Entity>,
pub effect_type: EffectType,
pub target: Targets,
}
/// Adds an effect to the effects queue
pub fn add_effect(source: Option<Entity>, 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<EffectSpawner> = 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),
_ => {}
}
}

15
src/effects/particles.rs Normal file
View file

@ -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::<Map>();
let mut particle_builder = ecs.fetch_mut::<ParticleBuilder>();
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);
}
}
}

10
src/effects/targeting.rs Normal file
View file

@ -0,0 +1,10 @@
use crate::{Map, Position};
use specs::prelude::*;
pub fn entity_position(ecs: &World, target: Entity) -> Option<usize> {
if let Some(position) = ecs.read_storage::<Position>().get(target) {
let map = ecs.fetch::<Map>();
return Some(map.xy_idx(position.x, position.y));
}
return None;
}