1use common::{
2 CachedSpatialGrid, Damage, DamageKind, DamageSource, Explosion, RadiusEffect,
3 comp::{Body, Object, PhysicsState, Pos, Teleporting, Vel, object},
4 consts::TELEPORTER_RADIUS,
5 effect::Effect,
6 event::{ChangeBodyEvent, DeleteEvent, EmitExt, EventBus, ExplosionEvent, ShootEvent},
7 event_emitters,
8 outcome::Outcome,
9 resources::{DeltaTime, Time},
10};
11use common_ecs::{Job, Origin, Phase, System};
12use specs::{Entities, Join, LendJoin, Read, ReadStorage};
13use vek::Rgb;
14
15event_emitters! {
16 struct Events[Emitters] {
17 delete: DeleteEvent,
18 explosion: ExplosionEvent,
19 shoot: ShootEvent,
20 change_body: ChangeBodyEvent,
21 }
22}
23
24#[derive(Default)]
26pub struct Sys;
27impl<'a> System<'a> for Sys {
28 type SystemData = (
29 Entities<'a>,
30 Events<'a>,
31 Read<'a, DeltaTime>,
32 Read<'a, Time>,
33 Read<'a, EventBus<Outcome>>,
34 Read<'a, CachedSpatialGrid>,
35 ReadStorage<'a, Pos>,
36 ReadStorage<'a, Vel>,
37 ReadStorage<'a, PhysicsState>,
38 ReadStorage<'a, Object>,
39 ReadStorage<'a, Body>,
40 ReadStorage<'a, Teleporting>,
41 );
42
43 const NAME: &'static str = "object";
44 const ORIGIN: Origin = Origin::Server;
45 const PHASE: Phase = Phase::Create;
46
47 fn run(
48 _job: &mut Job<Self>,
49 (
50 entities,
51 events,
52 _dt,
53 time,
54 outcome_bus,
55 spatial_grid,
56 positions,
57 velocities,
58 physics_states,
59 objects,
60 bodies,
61 teleporting,
62 ): Self::SystemData,
63 ) {
64 let mut emitters = events.get_emitters();
65
66 for (entity, pos, vel, physics, object, body) in (
68 &entities,
69 &positions,
70 &velocities,
71 physics_states.maybe(),
72 &objects,
73 bodies.maybe(),
74 )
75 .join()
76 {
77 match object {
78 Object::Bomb { owner } => {
79 if physics.is_some_and(|physics| physics.on_surface().is_some()) {
80 emitters.emit(DeleteEvent(entity));
81 emitters.emit(ExplosionEvent {
82 pos: pos.0,
83 explosion: Explosion {
84 effects: vec![
85 RadiusEffect::Entity(Effect::Damage(Damage {
86 source: DamageSource::Explosion,
87 kind: DamageKind::Energy,
88 value: 40.0,
89 })),
90 RadiusEffect::Entity(Effect::Poise(-100.0)),
91 RadiusEffect::TerrainDestruction(4.0, Rgb::black()),
92 ],
93 radius: 12.0,
94 reagent: None,
95 min_falloff: 0.75,
96 },
97 owner: *owner,
98 });
99 }
100 },
101 Object::SurpriseEgg { .. } => {
102 if physics.is_some_and(|physics| physics.on_surface().is_some()) {
103 emitters.emit(DeleteEvent(entity));
104 outcome_bus.emit_now(Outcome::SurpriseEgg { pos: pos.0 });
105 }
106 },
107 Object::Firework { owner, reagent } => {
108 if vel.0.z < 0.0 {
109 const ENABLE_RECURSIVE_FIREWORKS: bool = true;
110 if ENABLE_RECURSIVE_FIREWORKS {
111 use common::{
112 comp::{LightEmitter, Projectile},
113 util::Dir,
114 };
115 use rand::Rng;
116 use std::{f32::consts::PI, time::Duration};
117 use vek::Vec3;
118 let mut rng = rand::thread_rng();
119 let thresholds: &[(f32, usize)] = &[(0.25, 2), (0.7, 1)];
122 let expected = {
123 let mut total = 0.0;
124 let mut cumulative_probability = 0.0;
125 for (p, n) in thresholds {
126 total += (p - cumulative_probability) * *n as f32;
127 cumulative_probability += p;
128 }
129 total
130 };
131 assert!(expected < 1.0);
132 let num_fireworks = (|| {
133 let x = rng.gen_range(0.0..1.0);
134 for (p, n) in thresholds {
135 if x < *p {
136 return *n;
137 }
138 }
139 0
140 })();
141 for _ in 0..num_fireworks {
142 let speed: f32 = rng.gen_range(40.0..80.0);
143 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
144 let phi: f32 = rng.gen_range(0.25 * PI..0.5 * PI);
145 let dir = Dir::from_unnormalized(Vec3::new(
146 theta.cos(),
147 theta.sin(),
148 phi.sin(),
149 ))
150 .expect("nonzero vector should normalize");
151 emitters.emit(ShootEvent {
152 entity,
153 pos: *pos,
154 dir,
155 body: Body::Object(object::Body::for_firework(*reagent)),
156 light: Some(LightEmitter {
157 animated: true,
158 flicker: 2.0,
159 strength: 2.0,
160 col: Rgb::new(1.0, 1.0, 0.0),
161 }),
162 projectile: Projectile {
163 hit_solid: Vec::new(),
164 hit_entity: Vec::new(),
165 time_left: Duration::from_secs(60),
166 owner: *owner,
167 ignore_group: true,
168 is_sticky: true,
169 is_point: true,
170 },
171 speed,
172 object: Some(Object::Firework {
173 owner: *owner,
174 reagent: *reagent,
175 }),
176 });
177 }
178 }
179 emitters.emit(DeleteEvent(entity));
180 emitters.emit(ExplosionEvent {
181 pos: pos.0,
182 explosion: Explosion {
183 effects: vec![
184 RadiusEffect::Entity(Effect::Damage(Damage {
185 source: DamageSource::Explosion,
186 kind: DamageKind::Energy,
187 value: 5.0,
188 })),
189 RadiusEffect::Entity(Effect::Poise(-40.0)),
190 RadiusEffect::TerrainDestruction(4.0, Rgb::black()),
191 ],
192 radius: 12.0,
193 reagent: Some(*reagent),
194 min_falloff: 0.0,
195 },
196 owner: *owner,
197 });
198 }
199 },
200 Object::DeleteAfter {
201 spawned_at,
202 timeout,
203 } => {
204 if (time.0 - spawned_at.0).max(0.0) > timeout.as_secs_f64() {
205 emitters.emit(DeleteEvent(entity));
206 }
207 },
208 Object::Portal { .. } => {
209 let is_active = spatial_grid
210 .0
211 .in_circle_aabr(pos.0.xy(), TELEPORTER_RADIUS)
212 .any(|entity| {
213 (&positions, &teleporting)
214 .lend_join()
215 .get(entity, &entities)
216 .is_some_and(|(teleporter_pos, _)| {
217 pos.0.distance_squared(teleporter_pos.0)
218 <= TELEPORTER_RADIUS.powi(2)
219 })
220 });
221
222 if body.is_some_and(|body| {
223 (*body == Body::Object(object::Body::PortalActive)) != is_active
224 }) {
225 emitters.emit(ChangeBodyEvent {
226 entity,
227 new_body: Body::Object(if is_active {
228 outcome_bus.emit_now(Outcome::PortalActivated { pos: pos.0 });
229 object::Body::PortalActive
230 } else {
231 object::Body::Portal
232 }),
233 });
234 }
235 },
236 }
237 }
238 }
239}