1use common::{
2 CachedSpatialGrid, Damage, DamageKind, GroupTarget,
3 combat::{Attack, AttackDamage},
4 comp::{Body, Object, Pos, Teleporting, Vel, beam, object},
5 consts::TELEPORTER_RADIUS,
6 event::{ChangeBodyEvent, DeleteEvent, EmitExt, EventBus},
7 event_emitters,
8 outcome::Outcome,
9 resources::{DeltaTime, Secs, Time},
10 states::basic_summon::BeamPillarIndicatorSpecifier,
11};
12use common_ecs::{Job, Origin, Phase, System};
13use hashbrown::HashMap;
14use specs::{Entities, Join, LazyUpdate, LendJoin, Read, ReadStorage, WriteStorage};
15use vek::{QuadraticBezier3, Vec3};
16
17event_emitters! {
18 struct Events[Emitters] {
19 delete: DeleteEvent,
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 WriteStorage<'a, Object>,
38 ReadStorage<'a, Body>,
39 ReadStorage<'a, Teleporting>,
40 ReadStorage<'a, beam::Beam>,
41 Read<'a, LazyUpdate>,
42 );
43
44 const NAME: &'static str = "object";
45 const ORIGIN: Origin = Origin::Server;
46 const PHASE: Phase = Phase::Create;
47
48 fn run(
49 _job: &mut Job<Self>,
50 (
51 entities,
52 events,
53 dt,
54 time,
55 outcome_bus,
56 spatial_grid,
57 positions,
58 velocities,
59 mut objects,
60 bodies,
61 teleporting,
62 beams,
63 updater,
64 ): Self::SystemData,
65 ) {
66 let mut emitters = events.get_emitters();
67
68 for (entity, pos, vel, object, body) in (
70 &entities,
71 &positions,
72 velocities.maybe(),
73 &mut objects,
74 bodies.maybe(),
75 )
76 .join()
77 {
78 match object {
79 Object::DeleteAfter {
80 spawned_at,
81 timeout,
82 } => {
83 if (time.0 - spawned_at.0).max(0.0) > timeout.as_secs_f64() {
84 emitters.emit(DeleteEvent(entity));
85 }
86 },
87 Object::Portal { .. } => {
88 let is_active = spatial_grid
89 .0
90 .in_circle_aabr(pos.0.xy(), TELEPORTER_RADIUS)
91 .any(|entity| {
92 (&positions, &teleporting)
93 .lend_join()
94 .get(entity, &entities)
95 .is_some_and(|(teleporter_pos, _)| {
96 pos.0.distance_squared(teleporter_pos.0)
97 <= TELEPORTER_RADIUS.powi(2)
98 })
99 });
100
101 if body.is_some_and(|body| {
102 (*body == Body::Object(object::Body::PortalActive)) != is_active
103 }) {
104 emitters.emit(ChangeBodyEvent {
105 entity,
106 new_body: Body::Object(if is_active {
107 outcome_bus.emit_now(Outcome::PortalActivated { pos: pos.0 });
108 object::Body::PortalActive
109 } else {
110 object::Body::Portal
111 }),
112 permanent_change: None,
113 });
114 }
115 },
116 Object::BeamPillar {
117 spawned_at,
118 buildup_duration,
119 attack_duration,
120 beam_duration,
121 radius,
122 height,
123 damage,
124 damage_effect,
125 dodgeable,
126 tick_rate,
127 specifier,
128 indicator_specifier,
129 } => {
130 match indicator_specifier {
131 BeamPillarIndicatorSpecifier::FirePillar => {
132 outcome_bus.emit_now(Outcome::FirePillarIndicator {
133 pos: pos.0,
134 radius: *radius,
135 })
136 },
137 }
138
139 let age = (time.0 - spawned_at.0).max(0.0);
140 let buildup = buildup_duration.as_secs_f64();
141 let attack = attack_duration.as_secs_f64();
142
143 if age > buildup + attack {
144 emitters.emit(DeleteEvent(entity));
145 } else if age > buildup && !beams.contains(entity) {
146 let mut attack_damage = AttackDamage::new(
147 Damage {
148 kind: DamageKind::Energy,
149 value: *damage,
150 },
151 Some(GroupTarget::OutOfGroup),
152 rand::random(),
153 );
154 if let Some(combat_effect) = damage_effect {
155 attack_damage = attack_damage.with_effect(combat_effect.clone());
156 }
157
158 updater.insert(entity, beam::Beam {
159 attack: Attack::new(None).with_damage(attack_damage),
160 dodgeable: *dodgeable,
161 start_radius: *radius,
162 end_radius: *radius,
163 range: *height,
164 duration: Secs(beam_duration.as_secs_f64()),
165 tick_dur: Secs(1.0 / *tick_rate as f64),
166 hit_entities: Vec::new(),
167 hit_durations: HashMap::new(),
168 specifier: *specifier,
169 bezier: QuadraticBezier3 {
170 start: pos.0,
171 ctrl: pos.0,
172 end: pos.0,
173 },
174 });
175 }
176 },
177 Object::Crux { pid_controller, .. } => {
178 if let Some(vel) = vel
179 && let Some(pid_controller) = pid_controller
180 && let Some(accel) = body.and_then(|body| {
181 body.fly_thrust()
182 .map(|fly_thrust| fly_thrust / body.mass().0)
183 })
184 {
185 pid_controller.add_measurement(time.0, pos.0.z);
186 let dir = pid_controller.calc_err();
187 pid_controller.limit_integral_windup(|z| *z = z.clamp(-1.0, 1.0));
188
189 updater
190 .insert(entity, Vel((vel.0.z + dir * accel * dt.0) * Vec3::unit_z()));
191 }
192 },
193 }
194 }
195 }
196}