veloren_common_systems/
aura.rs

1use std::collections::HashSet;
2
3use common::{
4    combat,
5    comp::{
6        Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Mass, Player, Pos, Stats,
7        aura::{AuraChange, AuraKey, AuraKind, AuraTarget, EnteredAuras},
8        buff::{Buff, BuffCategory, BuffChange, BuffSource, DestInfo},
9        group::Group,
10    },
11    event::{AuraEvent, BuffEvent, EmitExt},
12    event_emitters,
13    resources::Time,
14    uid::{IdMaps, Uid},
15};
16use common_ecs::{Job, Origin, Phase, System};
17use specs::{Entities, Entity as EcsEntity, Join, Read, ReadStorage, SystemData, shred};
18
19event_emitters! {
20    struct Events[Emitters] {
21        aura: AuraEvent,
22        buff: BuffEvent,
23    }
24}
25
26#[derive(SystemData)]
27pub struct ReadData<'a> {
28    entities: Entities<'a>,
29    players: ReadStorage<'a, Player>,
30    time: Read<'a, Time>,
31    events: Events<'a>,
32    id_maps: Read<'a, IdMaps>,
33    cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
34    positions: ReadStorage<'a, Pos>,
35    char_states: ReadStorage<'a, CharacterState>,
36    alignments: ReadStorage<'a, Alignment>,
37    healths: ReadStorage<'a, Health>,
38    groups: ReadStorage<'a, Group>,
39    uids: ReadStorage<'a, Uid>,
40    stats: ReadStorage<'a, Stats>,
41    buffs: ReadStorage<'a, Buffs>,
42    auras: ReadStorage<'a, Auras>,
43    entered_auras: ReadStorage<'a, EnteredAuras>,
44    masses: ReadStorage<'a, Mass>,
45}
46
47#[derive(Default)]
48pub struct Sys;
49impl<'a> System<'a> for Sys {
50    type SystemData = ReadData<'a>;
51
52    const NAME: &'static str = "aura";
53    const ORIGIN: Origin = Origin::Common;
54    const PHASE: Phase = Phase::Create;
55
56    fn run(_job: &mut Job<Self>, read_data: Self::SystemData) {
57        let mut emitters = read_data.events.get_emitters();
58        let mut active_auras: HashSet<(Uid, Uid, AuraKey)> = HashSet::new();
59
60        // Iterate through all entities with an aura
61        for (entity, pos, auras_comp, uid) in (
62            &read_data.entities,
63            &read_data.positions,
64            &read_data.auras,
65            &read_data.uids,
66        )
67            .join()
68        {
69            let mut expired_auras = Vec::<AuraKey>::new();
70            // Iterate through the auras attached to this entity
71            for (key, aura) in auras_comp.auras.iter() {
72                // Tick the aura and subtract dt from it
73                if let Some(end_time) = aura.end_time {
74                    if read_data.time.0 > end_time.0 {
75                        expired_auras.push(key);
76                    }
77                }
78                let target_iter = read_data
79                    .cached_spatial_grid
80                    .0
81                    .in_circle_aabr(pos.0.xy(), aura.radius)
82                    .filter_map(|target| {
83                        read_data.positions.get(target).and_then(|target_pos| {
84                            Some((
85                                target,
86                                target_pos,
87                                read_data.healths.get(target)?,
88                                read_data.uids.get(target)?,
89                                read_data.entered_auras.get(target)?,
90                            ))
91                        })
92                    });
93                target_iter.for_each(|(target, target_pos, health, target_uid, entered_auras)| {
94                    let target_buffs = match read_data.buffs.get(target) {
95                        Some(buff) => buff,
96                        None => return,
97                    };
98
99                    // Ensure entity is within the aura radius
100                    if target_pos.0.distance_squared(pos.0) < aura.radius.powi(2) {
101                        // Ensure the entity is in the group we want to target
102                        let same_group = |uid: Uid| {
103                            read_data
104                                .id_maps
105                                .uid_entity(uid)
106                                .and_then(|e| read_data.groups.get(e))
107                                .is_some_and(|owner_group| {
108                                    Some(owner_group) == read_data.groups.get(target)
109                                })
110                                || *target_uid == uid
111                        };
112
113                        let allow_friendly_fire =
114                            combat::allow_friendly_fire(&read_data.entered_auras, entity, target);
115
116                        if !(allow_friendly_fire && entity != target
117                            || match aura.target {
118                                AuraTarget::GroupOf(uid) => same_group(uid),
119                                AuraTarget::NotGroupOf(uid) => !same_group(uid),
120                                AuraTarget::All => true,
121                            })
122                        {
123                            return;
124                        }
125
126                        let did_activate = activate_aura(
127                            key,
128                            aura,
129                            entity,
130                            *uid,
131                            target,
132                            health,
133                            target_buffs,
134                            allow_friendly_fire,
135                            &read_data,
136                            &mut emitters,
137                        );
138
139                        if did_activate {
140                            if entered_auras
141                                .auras
142                                .get(aura.aura_kind.as_ref())
143                                .is_none_or(|auras| !auras.contains(&(*uid, key)))
144                            {
145                                emitters.emit(AuraEvent {
146                                    entity: target,
147                                    aura_change: AuraChange::EnterAura(
148                                        *uid,
149                                        key,
150                                        *aura.aura_kind.as_ref(),
151                                    ),
152                                });
153                            }
154                            active_auras.insert((*uid, *target_uid, key));
155                        }
156                    }
157                });
158            }
159            if !expired_auras.is_empty() {
160                emitters.emit(AuraEvent {
161                    entity,
162                    aura_change: AuraChange::RemoveByKey(expired_auras),
163                });
164            }
165        }
166
167        for (entity, entered_auras, uid) in (
168            &read_data.entities,
169            &read_data.entered_auras,
170            &read_data.uids,
171        )
172            .join()
173            .filter(|(_, active_auras, _)| !active_auras.auras.is_empty())
174        {
175            emitters.emit_many(
176                entered_auras
177                    .auras
178                    .iter()
179                    .flat_map(|(variant, entered_auras)| {
180                        entered_auras.iter().zip(core::iter::repeat(*variant))
181                    })
182                    .filter_map(|((caster_uid, key), variant)| {
183                        (!active_auras.contains(&(*caster_uid, *uid, *key))).then_some(AuraEvent {
184                            entity,
185                            aura_change: AuraChange::ExitAura(*caster_uid, *key, variant),
186                        })
187                    }),
188            );
189        }
190    }
191}
192
193#[warn(clippy::pedantic)]
194//#[warn(clippy::nursery)]
195fn activate_aura(
196    key: AuraKey,
197    aura: &Aura,
198    applier: EcsEntity,
199    applier_uid: Uid,
200    target: EcsEntity,
201    health: &Health,
202    target_buffs: &Buffs,
203    allow_friendly_fire: bool,
204    read_data: &ReadData,
205    emitters: &mut impl EmitExt<BuffEvent>,
206) -> bool {
207    let should_activate = match aura.aura_kind {
208        AuraKind::Buff { kind, source, .. } => {
209            let conditions_held = match kind {
210                BuffKind::CampfireHeal => {
211                    // true if sitting or if owned and owner is sitting + not full health
212                    health.current() < health.maximum()
213                        && (read_data
214                            .char_states
215                            .get(target)
216                            .is_some_and(CharacterState::is_sitting)
217                            || read_data
218                                .alignments
219                                .get(target)
220                                .and_then(|alignment| match alignment {
221                                    Alignment::Owned(uid) => Some(uid),
222                                    _ => None,
223                                })
224                                .and_then(|uid| read_data.id_maps.uid_entity(*uid))
225                                .and_then(|owner| read_data.char_states.get(owner))
226                                .is_some_and(CharacterState::is_sitting))
227                },
228                // Add other specific buff conditions here
229                _ => true,
230            };
231
232            // TODO: this check will disable friendly fire with PvE switch.
233            //
234            // Which means that you can't apply debuffs on you and your group
235            // even if it's intended mechanic.
236            //
237            // We don't have this for now, but think about this
238            // when we will add this.
239            let permit_pvp = || {
240                let owner = match source {
241                    BuffSource::Character { by } => read_data.id_maps.uid_entity(by),
242                    _ => None,
243                };
244                combat::permit_pvp(
245                    &read_data.alignments,
246                    &read_data.players,
247                    &read_data.entered_auras,
248                    &read_data.id_maps,
249                    owner,
250                    target,
251                )
252            };
253
254            conditions_held && (kind.is_buff() || allow_friendly_fire || permit_pvp())
255        },
256        AuraKind::FriendlyFire => true,
257        AuraKind::ForcePvP => {
258            // Only apply this aura to players
259            read_data.players.contains(target)
260        },
261    };
262
263    if !should_activate {
264        return false;
265    }
266
267    // TODO: When more aura kinds (besides Buff) are
268    // implemented, match on them here
269    match aura.aura_kind {
270        AuraKind::Buff {
271            kind,
272            data,
273            category,
274            source,
275        } => {
276            // Checks that target is not already receiving a buff
277            // from an aura, where the buff is of the same kind,
278            // and is of at least the same strength
279            // and of at least the same duration.
280            // If no such buff is present, adds the buff.
281            let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| {
282                buff.cat_ids
283                    .iter()
284                    .any(|cat_id| matches!(cat_id, BuffCategory::FromActiveAura(uid, aura_key) if *aura_key == key && *uid == applier_uid))
285                    && buff.kind == kind
286                    && buff.data.strength >= data.strength
287            });
288            if emit_buff {
289                let dest_info = DestInfo {
290                    stats: read_data.stats.get(target),
291                    mass: read_data.masses.get(target),
292                };
293                emitters.emit(BuffEvent {
294                    entity: target,
295                    buff_change: BuffChange::Add(Buff::new(
296                        kind,
297                        data,
298                        vec![category, BuffCategory::FromActiveAura(applier_uid, key)],
299                        source,
300                        *read_data.time,
301                        dest_info,
302                        read_data.masses.get(applier),
303                    )),
304                });
305            }
306        },
307        // No implementation needed for these auras
308        AuraKind::FriendlyFire | AuraKind::ForcePvP => {},
309    }
310
311    true
312}