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#[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 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 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 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 if arc.hit_entities.contains(uid_b) {
132 continue;
133 }
134
135 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 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 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 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 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 arc.hit_entities.push(*uid_b);
273 arc.last_arc_time = *read_data.time;
274 break;
275 }
276 }
277 })
278 }
279}