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 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 for (key, aura) in auras_comp.auras.iter() {
72 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 if target_pos.0.distance_squared(pos.0) < aura.radius.powi(2) {
101 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)]
194fn 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 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 _ => true,
230 };
231
232 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 read_data.players.contains(target)
260 },
261 };
262
263 if !should_activate {
264 return false;
265 }
266
267 match aura.aura_kind {
270 AuraKind::Buff {
271 kind,
272 data,
273 category,
274 source,
275 } => {
276 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 AuraKind::FriendlyFire | AuraKind::ForcePvP => {},
309 }
310
311 true
312}