1use crate::{
2 combat,
3 comp::{
4 Body, CharacterState, FrontendMarker, LightEmitter, Pos, StateUpdate,
5 ability::Amount,
6 character_state::OutputEvents,
7 object::Body::{FireRing, GrenadeClay, LaserBeam, LaserBeamSmall},
8 projectile::{ProjectileConstructor, aim_projectile},
9 },
10 event::{LocalEvent, ShootEvent},
11 outcome::Outcome,
12 states::{
13 behavior::{CharacterBehavior, JoinData},
14 utils::*,
15 },
16 util::Dir,
17};
18use itertools::Either;
19use rand::rng;
20use serde::{Deserialize, Serialize};
21use std::time::Duration;
22
23#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
25pub struct StaticData {
26 pub buildup_duration: Duration,
28 pub recover_duration: Duration,
30 pub projectile_spread: Option<ProjectileSpread>,
32 pub projectile: ProjectileConstructor,
34 pub projectile_body: Body,
35 pub projectile_light: Option<LightEmitter>,
36 pub projectile_speed: f32,
37 pub num_projectiles: Amount,
39 pub ability_info: AbilityInfo,
41 pub movement_modifier: MovementModifier,
43 pub ori_modifier: OrientationModifier,
45 pub auto_aim: bool,
48 pub vertical_angle_offset: f32,
52 pub marker: Option<FrontendMarker>,
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub struct Data {
58 pub static_data: StaticData,
61 pub timer: Duration,
63 pub stage_section: StageSection,
65 pub exhausted: bool,
67 pub movement_modifier: Option<f32>,
69 pub ori_modifier: Option<f32>,
71}
72
73impl CharacterBehavior for Data {
74 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
75 let mut update = StateUpdate::from(data);
76
77 handle_orientation(data, &mut update, self.ori_modifier.unwrap_or(1.0), None);
78 handle_move(data, &mut update, self.movement_modifier.unwrap_or(0.7));
79 handle_jump(data, output_events, &mut update, 1.0);
80
81 match self.stage_section {
82 StageSection::Buildup => {
83 if self.timer < self.static_data.buildup_duration {
84 if let CharacterState::BasicRanged(c) = &mut update.character {
86 c.timer = tick_attack_or_default(data, self.timer, None);
87 }
88 match self.static_data.projectile_body {
89 Body::Object(LaserBeam) => {
90 output_events.emit_local(LocalEvent::CreateOutcome(
92 Outcome::CyclopsCharge {
93 pos: data.pos.0
94 + *data.ori.look_dir() * (data.body.max_radius()),
95 },
96 ));
97 },
98 Body::Object(GrenadeClay) => {
99 output_events.emit_local(LocalEvent::CreateOutcome(
101 Outcome::FuseCharge {
102 pos: data.pos.0
103 + *data.ori.look_dir() * (2.5 * data.body.max_radius()),
104 },
105 ));
106 },
107 Body::Object(LaserBeamSmall) => {
108 output_events.emit_local(LocalEvent::CreateOutcome(
109 Outcome::TerracottaStatueCharge {
110 pos: data.pos.0
111 + *data.ori.look_dir() * (data.body.max_radius()),
112 },
113 ));
114 },
115 Body::Object(FireRing) if self.timer == Duration::default() => {
116 output_events.emit_local(LocalEvent::CreateOutcome(
117 Outcome::FireBreathCharge {
118 pos: data.pos.0
119 + *data.ori.look_dir() * (data.body.max_radius()),
120 },
121 ));
122 },
123 _ => {},
124 }
125 } else {
126 if let CharacterState::BasicRanged(c) = &mut update.character {
128 c.timer = Duration::default();
129 c.stage_section = StageSection::Recover;
130 c.movement_modifier = c.static_data.movement_modifier.recover;
131 c.ori_modifier = c.static_data.ori_modifier.recover;
132 }
133 }
134 },
135 StageSection::Recover => {
136 if !self.exhausted {
137 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
139 let projectile = self.static_data.projectile.clone().create_projectile(
140 Some(*data.uid),
141 precision_mult,
142 Some(self.static_data.ability_info),
143 );
144 let num_projectiles = self
146 .static_data
147 .num_projectiles
148 .compute(data.heads.map_or(1, |heads| heads.amount() as u32));
149
150 let mut rng = rng();
151
152 let aim_dir = if self.static_data.ori_modifier.buildup.is_some() {
153 data.inputs.look_dir.merge_z(data.ori.look_dir())
154 } else {
155 data.inputs.look_dir
156 };
157
158 let aim_dir = if self.static_data.vertical_angle_offset != 0.0 {
161 let cross = vek::Vec3::unit_z().cross(*aim_dir).normalized();
162 Dir::from_unnormalized(
163 vek::Quaternion::rotation_3d(
164 -self.static_data.vertical_angle_offset,
165 cross,
166 ) * *aim_dir,
167 )
168 .unwrap_or(aim_dir)
169 } else {
170 aim_dir
171 };
172
173 let body_offsets = data
175 .body
176 .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
177 let pos = Pos(data.pos.0 + body_offsets);
178
179 let aim_dir = if self.static_data.auto_aim
180 && let Some(sel_pos) = self
181 .static_data
182 .ability_info
183 .input_attr
184 .and_then(|ia| ia.select_pos)
185 {
186 if let Some(ideal_dir) =
187 aim_projectile(self.static_data.projectile_speed, pos.0, sel_pos, true)
188 {
189 ideal_dir
190 } else {
191 aim_dir
192 }
193 } else {
194 aim_dir
195 };
196
197 let dirs = if let Some(spread) = self.static_data.projectile_spread {
198 Either::Left(spread.compute_directions(
199 aim_dir,
200 *data.ori,
201 num_projectiles,
202 &mut rng,
203 ))
204 } else {
205 Either::Right((0..num_projectiles).map(|_| aim_dir))
206 };
207
208 for dir in dirs {
209 output_events.emit_server(ShootEvent {
211 entity: Some(data.entity),
212 source_vel: Some(*data.vel),
213 pos,
214 dir,
215 body: self.static_data.projectile_body,
216 projectile: projectile.clone(),
217 light: self.static_data.projectile_light,
218 speed: self.static_data.projectile_speed,
219 object: None,
220 marker: self.static_data.marker,
221 });
222 }
223
224 if let CharacterState::BasicRanged(c) = &mut update.character {
225 c.exhausted = true;
226 }
227 } else if self.timer < self.static_data.recover_duration {
228 if let CharacterState::BasicRanged(c) = &mut update.character {
230 c.timer = tick_attack_or_default(
231 data,
232 self.timer,
233 Some(data.stats.recovery_speed_modifier),
234 );
235 }
236 } else {
237 if input_is_pressed(data, self.static_data.ability_info.input) {
239 reset_state(self, data, output_events, &mut update);
240 } else {
241 end_ability(data, &mut update);
242 }
243 }
244 },
245 _ => {
246 end_ability(data, &mut update);
248 },
249 }
250
251 handle_interrupts(data, &mut update, output_events);
253
254 update
255 }
256}
257
258fn reset_state(
259 data: &Data,
260 join: &JoinData,
261 output_events: &mut OutputEvents,
262 update: &mut StateUpdate,
263) {
264 handle_input(
265 join,
266 output_events,
267 update,
268 data.static_data.ability_info.input,
269 );
270}