1use std::f32::consts::PI;
2
3use common::{
4 GroupTarget,
5 combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
6 comp::{
7 Alignment, Beam, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory,
8 Mass, Ori, Player, Pos, Scale, Stats,
9 agent::{Sound, SoundKind},
10 aura::EnteredAuras,
11 },
12 event::{self, EmitExt, EventBus},
13 event_emitters,
14 outcome::Outcome,
15 resources::{DeltaTime, Time},
16 terrain::TerrainGrid,
17 uid::{IdMaps, Uid},
18 vol::ReadVol,
19};
20use common_ecs::{Job, Origin, ParMode, Phase, System};
21use rand::Rng;
22use rayon::iter::ParallelIterator;
23use specs::{
24 Entities, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, WriteStorage, shred,
25};
26use vek::*;
27
28event_emitters! {
29 struct ReadAttackEvents[AttackEmitters] {
30 health_change: event::HealthChangeEvent,
31 energy_change: event::EnergyChangeEvent,
32 poise_change: event::PoiseChangeEvent,
33 sound: event::SoundEvent,
34 parry_hook: event::ParryHookEvent,
35 knockback: event::KnockbackEvent,
36 entity_attack_hoow: event::EntityAttackedHookEvent,
37 combo_change: event::ComboChangeEvent,
38 buff: event::BuffEvent,
39 }
40}
41
42#[derive(SystemData)]
43pub struct ReadData<'a> {
44 entities: Entities<'a>,
45 players: ReadStorage<'a, Player>,
46 time: Read<'a, Time>,
47 dt: Read<'a, DeltaTime>,
48 terrain: ReadExpect<'a, TerrainGrid>,
49 id_maps: Read<'a, IdMaps>,
50 cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
51 uids: ReadStorage<'a, Uid>,
52 positions: ReadStorage<'a, Pos>,
53 orientations: ReadStorage<'a, Ori>,
54 alignments: ReadStorage<'a, Alignment>,
55 scales: ReadStorage<'a, Scale>,
56 bodies: ReadStorage<'a, Body>,
57 healths: ReadStorage<'a, Health>,
58 inventories: ReadStorage<'a, Inventory>,
59 groups: ReadStorage<'a, Group>,
60 energies: ReadStorage<'a, Energy>,
61 stats: ReadStorage<'a, Stats>,
62 combos: ReadStorage<'a, Combo>,
63 character_states: ReadStorage<'a, CharacterState>,
64 buffs: ReadStorage<'a, Buffs>,
65 entered_auras: ReadStorage<'a, EnteredAuras>,
66 outcomes: Read<'a, EventBus<Outcome>>,
67 events: ReadAttackEvents<'a>,
68 masses: ReadStorage<'a, Mass>,
69}
70
71#[derive(Default)]
73pub struct Sys;
74impl<'a> System<'a> for Sys {
75 type SystemData = (ReadData<'a>, WriteStorage<'a, Beam>);
76
77 const NAME: &'static str = "beam";
78 const ORIGIN: Origin = Origin::Common;
79 const PHASE: Phase = Phase::Create;
80
81 fn run(job: &mut Job<Self>, (read_data, mut beams): Self::SystemData) {
82 let mut outcomes_emitter = read_data.outcomes.emitter();
83
84 (
85 &read_data.positions,
86 &read_data.orientations,
87 &read_data.character_states,
88 &mut beams,
89 )
90 .lend_join()
91 .for_each(|(pos, ori, char_state, mut beam)| {
92 if read_data.time.0 % beam.tick_dur.0 < read_data.dt.0 as f64 {
94 let (hit_entities, hit_durations) = beam.hit_entities_and_durations();
95 hit_durations.retain(|e, _| hit_entities.contains(e));
96 for entity in hit_entities {
97 *hit_durations.entry(*entity).or_insert(0) += 1;
98 }
99 beam.hit_entities.clear();
100 }
101 let (offset, target_dir) = if let CharacterState::BasicBeam(c) = char_state {
103 (c.beam_offset, c.aim_dir)
104 } else {
105 (Vec3::zero(), ori.look_dir())
106 };
107 beam.bezier.start = pos.0 + offset;
108 const REL_CTRL_DIST: f32 = 0.3;
109 let target_ctrl = beam.bezier.start + *target_dir * beam.range * REL_CTRL_DIST;
110 let ctrl_translate = (target_ctrl - beam.bezier.ctrl) * read_data.dt.0
111 / (beam.duration.0 as f32 * REL_CTRL_DIST);
112 beam.bezier.ctrl += ctrl_translate;
113 let target_end = beam.bezier.start + *target_dir * beam.range;
114 let end_translate =
115 (target_end - beam.bezier.end) * read_data.dt.0 / beam.duration.0 as f32;
116 beam.bezier.end += end_translate;
117 });
118
119 job.cpu_stats.measure(ParMode::Rayon);
120
121 let (_emitters, add_hit_entities, new_outcomes) = (
124 &read_data.entities,
125 &read_data.positions,
126 &read_data.orientations,
127 &read_data.uids,
128 &beams,
129 )
130 .par_join()
131 .fold(
132 || (read_data.events.get_emitters(), Vec::new(), Vec::new()),
133 |(mut emitters, mut add_hit_entities, mut outcomes),
134 (entity, pos, ori, uid, beam)| {
135 let mut rng = rand::thread_rng();
138 if rng.gen_bool(0.005) {
139 emitters.emit(event::SoundEvent {
140 sound: Sound::new(SoundKind::Beam, pos.0, 13.0, read_data.time.0),
141 });
142 }
143 outcomes.push(Outcome::Beam {
144 pos: pos.0,
145 specifier: beam.specifier,
146 });
147
148 let group = read_data.groups.get(entity);
151
152 let target_iter = read_data
154 .cached_spatial_grid
155 .0
156 .in_circle_aabr(beam.bezier.start.xy(), beam.range)
157 .filter_map(|target| {
158 read_data
159 .positions
160 .get(target)
161 .and_then(|l| read_data.healths.get(target).map(|r| (l, r)))
162 .and_then(|l| read_data.uids.get(target).map(|r| (l, r)))
163 .and_then(|l| read_data.bodies.get(target).map(|r| (l, r)))
164 .map(|(((pos_b, health_b), uid_b), body_b)| {
165 (target, uid_b, pos_b, health_b, body_b)
166 })
167 });
168 target_iter.for_each(|(target, uid_b, pos_b, health_b, body_b)| {
169 if beam.hit_entities.iter().any(|&e| e == target) {
171 return;
172 }
173
174 let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
176 let rad_b = body_b.max_radius() * scale_b;
177 let height_b = body_b.height() * scale_b;
178
179 let hit = entity != target
182 && !health_b.is_dead
183 && conical_bezier_cylinder_collision(
184 beam.bezier,
185 beam.end_radius,
186 beam.range,
187 pos_b.0,
188 rad_b,
189 height_b,
190 );
191
192 let tgt_dist = beam.bezier.start.distance(pos_b.0);
196 let beam_dir = (beam.bezier.ctrl - beam.bezier.start)
197 / beam.bezier.start.distance(beam.bezier.ctrl).max(0.01);
198 let hit = hit
199 && read_data
200 .terrain
201 .ray(
202 beam.bezier.start,
203 beam.bezier.start + beam_dir * (tgt_dist + 1.0),
204 )
205 .until(|b| b.is_filled())
206 .cast()
207 .0
208 >= tgt_dist;
209
210 if hit {
211 let allow_friendly_fire = combat::allow_friendly_fire(
212 &read_data.entered_auras,
213 entity,
214 target,
215 );
216
217 let same_group = group
219 .map(|group_a| Some(group_a) == read_data.groups.get(target))
220 .unwrap_or(false);
221
222 let target_group = if same_group {
223 GroupTarget::InGroup
224 } else {
225 GroupTarget::OutOfGroup
226 };
227
228 let attacker_info = Some(AttackerInfo {
229 entity,
230 uid: *uid,
231 group: read_data.groups.get(entity),
232 energy: read_data.energies.get(entity),
233 combo: read_data.combos.get(entity),
234 inventory: read_data.inventories.get(entity),
235 stats: read_data.stats.get(entity),
236 mass: read_data.masses.get(entity),
237 });
238
239 let target_info = TargetInfo {
240 entity: target,
241 uid: *uid_b,
242 inventory: read_data.inventories.get(target),
243 stats: read_data.stats.get(target),
244 health: read_data.healths.get(target),
245 pos: pos_b.0,
246 ori: read_data.orientations.get(target),
247 char_state: read_data.character_states.get(target),
248 energy: read_data.energies.get(target),
249 buffs: read_data.buffs.get(target),
250 mass: read_data.masses.get(target),
251 };
252
253 let target_dodging = read_data
254 .character_states
255 .get(target)
256 .and_then(|cs| cs.roll_attack_immunities())
257 .is_some_and(|i| i.beams);
258 let permit_pvp = combat::permit_pvp(
260 &read_data.alignments,
261 &read_data.players,
262 &read_data.entered_auras,
263 &read_data.id_maps,
264 Some(entity),
265 target,
266 );
267
268 let precision_from_flank = combat::precision_mult_from_flank(
269 beam.bezier.ctrl - beam.bezier.start,
270 target_info.ori,
271 Default::default(),
272 false,
273 );
274
275 let precision_from_time = {
276 if let Some(ticks) = beam.hit_durations.get(&target) {
277 let dur = *ticks as f32 * beam.tick_dur.0 as f32;
278 let mult =
279 (dur / combat::BEAM_DURATION_PRECISION).clamp(0.0, 1.0);
280 Some(combat::MAX_BEAM_DUR_PRECISION * mult)
281 } else {
282 None
283 }
284 };
285
286 let precision_mult = match (precision_from_flank, precision_from_time) {
287 (Some(a), Some(b)) => Some(a.max(b)),
288 (Some(a), None) | (None, Some(a)) => Some(a),
289 (None, None) => None,
290 };
291
292 let attack_options = AttackOptions {
293 target_dodging,
294 permit_pvp,
295 allow_friendly_fire,
296 target_group,
297 precision_mult,
298 };
299
300 beam.attack.apply_attack(
301 attacker_info,
302 &target_info,
303 ori.look_dir(),
304 attack_options,
305 1.0,
306 AttackSource::Beam,
307 *read_data.time,
308 &mut emitters,
309 |o| outcomes.push(o),
310 &mut rng,
311 0,
312 );
313
314 add_hit_entities.push((entity, target));
315 }
316 });
317 (emitters, add_hit_entities, outcomes)
318 },
319 )
320 .reduce(
321 || (read_data.events.get_emitters(), Vec::new(), Vec::new()),
322 |(mut events_a, mut hit_entities_a, mut outcomes_a),
323 (events_b, mut hit_entities_b, mut outcomes_b)| {
324 events_a.append(events_b);
325 hit_entities_a.append(&mut hit_entities_b);
326 outcomes_a.append(&mut outcomes_b);
327 (events_a, hit_entities_a, outcomes_a)
328 },
329 );
330 job.cpu_stats.measure(ParMode::Single);
331
332 outcomes_emitter.emit_many(new_outcomes);
333
334 for (entity, hit_entity) in add_hit_entities {
335 if let Some(ref mut beam) = beams.get_mut(entity) {
336 beam.hit_entities.push(hit_entity);
337 }
338 }
339 }
340}
341
342fn conical_bezier_cylinder_collision(
344 bezier: QuadraticBezier3<f32>,
346 max_rad: f32, range: f32, bottom_pos_b: Vec3<f32>, rad_b: f32,
351 length_b: f32,
352) -> bool {
353 let center_pos_b = bottom_pos_b.with_z(bottom_pos_b.z + length_b / 2.0);
358 let (t, closest_pos) =
359 bezier.binary_search_point_by_steps(center_pos_b, (range * 5.0) as u16, 0.1);
360 let bezier_rad = t * max_rad;
361 let z_check = {
362 let dist = (closest_pos.z - center_pos_b.z).abs();
363 dist < bezier_rad + length_b / 2.0
364 };
365 let rad_check = {
366 let dist_sqrd = closest_pos.xy().distance_squared(center_pos_b.xy());
367 dist_sqrd < (bezier_rad + rad_b).powi(2)
368 };
369 let endpoints_check = {
370 let tangent = bezier.evaluate_derivative(t);
371 let dir = center_pos_b - closest_pos;
372 tangent.angle_between(dir) >= 0.45 * PI
375 };
376
377 z_check && rad_check && endpoints_check
378}