Files
veloren/common/systems/src/aura.rs
2023-06-04 12:58:16 -04:00

244 lines
9.0 KiB
Rust

use common::{
combat,
comp::{
aura::{AuraChange, AuraKey, AuraKind, AuraTarget},
buff::{Buff, BuffCategory, BuffChange, BuffSource},
group::Group,
Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos, Stats,
},
event::{Emitter, EventBus, ServerEvent},
resources::Time,
uid::{IdMaps, Uid},
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
shred::ResourceId, Entities, Entity as EcsEntity, Join, Read, ReadStorage, SystemData, World,
};
#[derive(SystemData)]
pub struct ReadData<'a> {
entities: Entities<'a>,
players: ReadStorage<'a, Player>,
time: Read<'a, Time>,
server_bus: Read<'a, EventBus<ServerEvent>>,
id_maps: Read<'a, IdMaps>,
cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
positions: ReadStorage<'a, Pos>,
char_states: ReadStorage<'a, CharacterState>,
alignments: ReadStorage<'a, Alignment>,
healths: ReadStorage<'a, Health>,
groups: ReadStorage<'a, Group>,
uids: ReadStorage<'a, Uid>,
stats: ReadStorage<'a, Stats>,
buffs: ReadStorage<'a, Buffs>,
auras: ReadStorage<'a, Auras>,
}
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = ReadData<'a>;
const NAME: &'static str = "aura";
const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, read_data: Self::SystemData) {
let mut server_emitter = read_data.server_bus.emitter();
// Iterate through all entities with an aura
for (entity, pos, auras_comp, uid) in (
&read_data.entities,
&read_data.positions,
&read_data.auras,
&read_data.uids,
)
.join()
{
let mut expired_auras = Vec::<AuraKey>::new();
// Iterate through the auras attached to this entity
for (key, aura) in auras_comp.auras.iter() {
// Tick the aura and subtract dt from it
if let Some(end_time) = aura.end_time {
if read_data.time.0 > end_time.0 {
expired_auras.push(key);
}
}
let target_iter = read_data
.cached_spatial_grid
.0
.in_circle_aabr(pos.0.xy(), aura.radius)
.filter_map(|target| {
read_data
.positions
.get(target)
.and_then(|l| read_data.healths.get(target).map(|r| (l, r)))
.and_then(|l| read_data.uids.get(target).map(|r| (l, r)))
.map(|((target_pos, health), target_uid)| {
(
target,
target_pos,
health,
target_uid,
read_data.stats.get(target),
)
})
});
target_iter.for_each(|(target, target_pos, health, target_uid, stats)| {
let target_buffs = match read_data.buffs.get(target) {
Some(buff) => buff,
None => return,
};
// Ensure entity is within the aura radius
if target_pos.0.distance_squared(pos.0) < aura.radius.powi(2) {
// Ensure the entity is in the group we want to target
let same_group = |uid: Uid| {
read_data
.id_maps
.uid_entity(uid)
.and_then(|e| read_data.groups.get(e))
.map_or(false, |owner_group| {
Some(owner_group) == read_data.groups.get(target)
})
|| *target_uid == uid
};
let is_target = match aura.target {
AuraTarget::GroupOf(uid) => same_group(uid),
AuraTarget::NotGroupOf(uid) => !same_group(uid),
AuraTarget::All => true,
};
if is_target {
activate_aura(
key,
aura,
*uid,
target,
health,
target_buffs,
stats,
&read_data,
&mut server_emitter,
);
}
}
});
}
if !expired_auras.is_empty() {
server_emitter.emit(ServerEvent::Aura {
entity,
aura_change: AuraChange::RemoveByKey(expired_auras),
});
}
}
}
}
#[warn(clippy::pedantic)]
//#[warn(clippy::nursery)]
fn activate_aura(
key: AuraKey,
aura: &Aura,
applier: Uid,
target: EcsEntity,
health: &Health,
target_buffs: &Buffs,
stats: Option<&Stats>,
read_data: &ReadData,
server_emitter: &mut Emitter<ServerEvent>,
) {
let should_activate = match aura.aura_kind {
AuraKind::Buff { kind, source, .. } => {
let conditions_held = match kind {
BuffKind::CampfireHeal => {
// true if sitting or if owned and owner is sitting + not full health
health.current() < health.maximum()
&& (read_data
.char_states
.get(target)
.map_or(false, CharacterState::is_sitting)
|| read_data
.alignments
.get(target)
.and_then(|alignment| match alignment {
Alignment::Owned(uid) => Some(uid),
_ => None,
})
.and_then(|uid| read_data.id_maps.uid_entity(*uid))
.and_then(|owner| read_data.char_states.get(owner))
.map_or(false, CharacterState::is_sitting))
},
// Add other specific buff conditions here
_ => true,
};
// TODO: this check will disable friendly fire with PvE switch.
//
// Which means that you can't apply debuffs on you and your group
// even if it's intended mechanic.
//
// We don't have this for now, but think about this
// when we will add this.
let may_harm = || {
let owner = match source {
BuffSource::Character { by } => read_data.id_maps.uid_entity(by),
_ => None,
};
combat::may_harm(
&read_data.alignments,
&read_data.players,
&read_data.id_maps,
owner,
target,
)
};
conditions_held && (kind.is_buff() || may_harm())
},
};
if !should_activate {
return;
}
// TODO: When more aura kinds (besides Buff) are
// implemented, match on them here
match aura.aura_kind {
AuraKind::Buff {
kind,
data,
category,
source,
} => {
// Checks that target is not already receiving a buff
// from an aura, where the buff is of the same kind,
// and is of at least the same strength
// and of at least the same duration.
// If no such buff is present, adds the buff.
let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| {
buff.cat_ids
.iter()
.any(|cat_id| matches!(cat_id, BuffCategory::FromActiveAura(uid, aura_key) if *aura_key == key && *uid == applier))
&& buff.kind == kind
&& buff.data.strength >= data.strength
});
if emit_buff {
server_emitter.emit(ServerEvent::Buff {
entity: target,
buff_change: BuffChange::Add(Buff::new(
kind,
data,
vec![category, BuffCategory::FromActiveAura(applier, key)],
source,
*read_data.time,
stats,
Some(health),
)),
});
}
},
}
}