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