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