veloren_common_systems/
arcing.rs

1use common::{
2    GroupTarget,
3    combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
4    comp::{
5        Alignment, Arcing, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory,
6        Mass, Ori, Player, Pos, Scale, Stats, aura::EnteredAuras,
7    },
8    event::{
9        BuffEvent, ComboChangeEvent, DeleteEvent, EmitExt, EnergyChangeEvent,
10        EntityAttackedHookEvent, EventBus, HealthChangeEvent, KnockbackEvent, ParryHookEvent,
11        PoiseChangeEvent, TransformEvent,
12    },
13    event_emitters,
14    outcome::Outcome,
15    resources::Time,
16    uid::{IdMaps, Uid},
17    util::Dir,
18};
19use common_ecs::{Job, Origin, Phase, System};
20use specs::{Entities, Join, LendJoin, Read, ReadStorage, SystemData, WriteStorage, shred};
21
22event_emitters! {
23    struct Events[Emitters] {
24        delete: DeleteEvent,
25        health_change: HealthChangeEvent,
26        energy_change: EnergyChangeEvent,
27        parry_hook: ParryHookEvent,
28        knockback: KnockbackEvent,
29        buff: BuffEvent,
30        poise_change: PoiseChangeEvent,
31        combo_change: ComboChangeEvent,
32        entity_attack_hook: EntityAttackedHookEvent,
33        transform: TransformEvent,
34    }
35}
36
37#[derive(SystemData)]
38pub struct ReadData<'a> {
39    entities: Entities<'a>,
40    events: Events<'a>,
41    time: Read<'a, Time>,
42    id_maps: Read<'a, IdMaps>,
43    groups: ReadStorage<'a, Group>,
44    uids: ReadStorage<'a, Uid>,
45    scales: ReadStorage<'a, Scale>,
46    entered_auras: ReadStorage<'a, EnteredAuras>,
47    healths: ReadStorage<'a, Health>,
48    bodies: ReadStorage<'a, Body>,
49    energies: ReadStorage<'a, Energy>,
50    combos: ReadStorage<'a, Combo>,
51    inventories: ReadStorage<'a, Inventory>,
52    stats: ReadStorage<'a, Stats>,
53    masses: ReadStorage<'a, Mass>,
54    orientations: ReadStorage<'a, Ori>,
55    character_states: ReadStorage<'a, CharacterState>,
56    buffs: ReadStorage<'a, Buffs>,
57    alignments: ReadStorage<'a, Alignment>,
58    players: ReadStorage<'a, Player>,
59}
60
61/// This system is responsible for hit detection of arcing attacks. Arcing
62/// attacks chain between nearby entities.
63#[derive(Default)]
64pub struct Sys;
65impl<'a> System<'a> for Sys {
66    type SystemData = (
67        ReadData<'a>,
68        WriteStorage<'a, Arcing>,
69        WriteStorage<'a, Pos>,
70        Read<'a, EventBus<Outcome>>,
71    );
72
73    const NAME: &'static str = "arc";
74    const ORIGIN: Origin = Origin::Common;
75    const PHASE: Phase = Phase::Create;
76
77    fn run(_job: &mut Job<Self>, (read_data, mut arcs, mut positions, outcomes): Self::SystemData) {
78        let mut emitters = read_data.events.get_emitters();
79        let mut outcomes_emitter = outcomes.emitter();
80        let mut rng = rand::rng();
81
82        (&read_data.entities, &mut arcs)
83            .lend_join()
84            .for_each(|(entity, mut arc)| {
85                // Delete arc entity if it should expire
86                if (read_data.time.0 > arc.last_arc_time.0 + arc.properties.max_delay.0)
87                    || ((arc.hit_entities.len() > arc.properties.arcs as usize)
88                        && (read_data.time.0 > arc.last_arc_time.0 + arc.properties.min_delay.0))
89                {
90                    emitters.emit(DeleteEvent(entity));
91                    return;
92                }
93
94                let last_target = arc
95                    .hit_entities
96                    .last()
97                    .and_then(|uid| read_data.id_maps.uid_entity(*uid));
98
99                // Update arc entity position to position of last hit target entity
100                let arc_pos =
101                    if let Some(tgt_pos) = last_target.and_then(|e| positions.get(e)).copied() {
102                        if let Some(pos) = positions.get_mut(entity) {
103                            *pos = tgt_pos;
104                            tgt_pos
105                        } else {
106                            return;
107                        }
108                    } else {
109                        return;
110                    };
111
112                // Skip hit detection if not yet min delay
113                if read_data.time.0 < arc.last_arc_time.0 + arc.properties.min_delay.0 {
114                    return;
115                }
116
117                let arc_owner = arc.owner.and_then(|uid| read_data.id_maps.uid_entity(uid));
118
119                let arc_group = arc_owner.and_then(|e| read_data.groups.get(e));
120
121                for (target, uid_b, pos_b, health_b, body_b) in (
122                    &read_data.entities,
123                    &read_data.uids,
124                    &positions,
125                    &read_data.healths,
126                    &read_data.bodies,
127                )
128                    .join()
129                {
130                    // Check to see if entity has already been hit
131                    if arc.hit_entities.contains(uid_b) {
132                        continue;
133                    }
134
135                    // TODO: use Capsule Prism instead of Cylinder
136                    let (arc_rad, arc_height) = if let Some(lt) = last_target {
137                        let body = read_data.bodies.get(lt);
138                        let scale = read_data.scales.get(lt).map_or(1.0, |s| s.0);
139                        (
140                            body.map_or(0.0, |b| b.max_radius() * scale),
141                            body.map(|b| b.height() * scale),
142                        )
143                    } else {
144                        (0.0, None)
145                    };
146
147                    let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
148                    let rad_b = body_b.max_radius() * scale_b;
149
150                    // If the z ranges of each cylinder overlap, there is no z delta and the
151                    // shortest path will be a horizontal line, otherwise the shortest path will go
152                    // from the bottom of one range to the top of the other range
153                    let z_delta = {
154                        let pos_bzh = pos_b.0.z + body_b.height() * scale_b;
155                        let tgt_range = pos_b.0.z..=pos_bzh;
156                        if let Some(arc_height) = arc_height {
157                            let arc_pos_zh = arc_pos.0.z + arc_height;
158                            let arc_range = arc_pos.0.z..=arc_pos_zh;
159                            if tgt_range.contains(&arc_pos.0.z)
160                                || tgt_range.contains(&arc_pos_zh)
161                                || arc_range.contains(&pos_b.0.z)
162                                || arc_range.contains(&pos_bzh)
163                            {
164                                0.0
165                            } else {
166                                (arc_pos.0.z - pos_bzh)
167                                    .abs()
168                                    .min((pos_b.0.z - arc_pos_zh).abs())
169                            }
170                        } else if tgt_range.contains(&arc_pos.0.z) {
171                            0.0
172                        } else {
173                            (pos_b.0.z - arc_pos.0.z)
174                                .abs()
175                                .min((pos_bzh - arc_pos.0.z).abs())
176                        }
177                    };
178
179                    // See if entities are in the same group
180                    let same_group = arc_group
181                        .map(|group_a| Some(group_a) == read_data.groups.get(target))
182                        .unwrap_or(Some(*uid_b) == arc.owner);
183
184                    let is_owner = Some(*uid_b) == arc.owner;
185
186                    let target_group = if same_group {
187                        GroupTarget::InGroup
188                    } else {
189                        GroupTarget::OutOfGroup
190                    };
191
192                    let hit = entity != target
193                        && !health_b.is_dead
194                        && (!is_owner || arc.properties.targets_owner)
195                        && arc_pos.0.distance_squared(pos_b.0) + z_delta.powi(2)
196                            < (arc.properties.distance + arc_rad + rad_b).powi(2);
197
198                    if hit {
199                        let allow_friendly_fire = arc_owner.is_some_and(|entity| {
200                            combat::allow_friendly_fire(&read_data.entered_auras, entity, target)
201                        });
202                        let dir = Dir::from_unnormalized(pos_b.0 - arc_pos.0).unwrap_or_default();
203
204                        let attacker_info =
205                            arc_owner.zip(arc.owner).map(|(entity, uid)| AttackerInfo {
206                                entity,
207                                uid,
208                                group: read_data.groups.get(entity),
209                                energy: read_data.energies.get(entity),
210                                combo: read_data.combos.get(entity),
211                                inventory: read_data.inventories.get(entity),
212                                stats: read_data.stats.get(entity),
213                                mass: read_data.masses.get(entity),
214                                pos: Some(arc_pos.0),
215                            });
216
217                        let target_info = TargetInfo {
218                            entity: target,
219                            uid: *uid_b,
220                            inventory: read_data.inventories.get(target),
221                            stats: read_data.stats.get(target),
222                            health: read_data.healths.get(target),
223                            pos: pos_b.0,
224                            ori: read_data.orientations.get(target),
225                            char_state: read_data.character_states.get(target),
226                            energy: read_data.energies.get(target),
227                            buffs: read_data.buffs.get(target),
228                            mass: read_data.masses.get(target),
229                            player: read_data.players.get(target),
230                        };
231
232                        let target_dodging = read_data
233                            .character_states
234                            .get(target)
235                            .and_then(|cs| cs.roll_attack_immunities())
236                            .is_some_and(|i| i.arcs);
237                        // PvP check
238                        let permit_pvp = combat::permit_pvp(
239                            &read_data.alignments,
240                            &read_data.players,
241                            &read_data.entered_auras,
242                            &read_data.id_maps,
243                            arc_owner,
244                            target,
245                        );
246                        // Arcs aren't precise, and thus cannot be a precise strike
247                        let precision_mult = None;
248                        let attack_options = AttackOptions {
249                            target_dodging,
250                            permit_pvp,
251                            allow_friendly_fire,
252                            target_group,
253                            precision_mult,
254                        };
255
256                        arc.properties.attack.apply_attack(
257                            attacker_info,
258                            &target_info,
259                            dir,
260                            attack_options,
261                            1.0,
262                            AttackSource::Arc,
263                            *read_data.time,
264                            &mut emitters,
265                            |o| outcomes_emitter.emit(o),
266                            &mut rng,
267                            0,
268                        );
269
270                        // Once we hit the first entity, break the loop since we only arc to a
271                        // single entity
272                        arc.hit_entities.push(*uid_b);
273                        arc.last_arc_time = *read_data.time;
274                        break;
275                    }
276                }
277            })
278    }
279}