1use crate::{
2 combat,
3 comp::{
4 Body, CharacterState, LightEmitter, Pos, StateUpdate,
5 ability::Amount,
6 character_state::OutputEvents,
7 object::Body::{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};
17use itertools::Either;
18use rand::rng;
19use serde::{Deserialize, Serialize};
20use std::time::Duration;
21
22#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24pub struct StaticData {
25 pub buildup_duration: Duration,
27 pub recover_duration: Duration,
29 pub projectile_spread: Option<ProjectileSpread>,
31 pub projectile: ProjectileConstructor,
33 pub projectile_body: Body,
34 pub projectile_light: Option<LightEmitter>,
35 pub projectile_speed: f32,
36 pub num_projectiles: Amount,
38 pub ability_info: AbilityInfo,
40 pub movement_modifier: MovementModifier,
42 pub ori_modifier: OrientationModifier,
44 pub auto_aim: bool,
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct Data {
51 pub static_data: StaticData,
54 pub timer: Duration,
56 pub stage_section: StageSection,
58 pub exhausted: bool,
60 pub movement_modifier: Option<f32>,
62 pub ori_modifier: Option<f32>,
64}
65
66impl CharacterBehavior for Data {
67 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
68 let mut update = StateUpdate::from(data);
69
70 handle_orientation(data, &mut update, self.ori_modifier.unwrap_or(1.0), None);
71 handle_move(data, &mut update, self.movement_modifier.unwrap_or(0.7));
72 handle_jump(data, output_events, &mut update, 1.0);
73
74 match self.stage_section {
75 StageSection::Buildup => {
76 if self.timer < self.static_data.buildup_duration {
77 if let CharacterState::BasicRanged(c) = &mut update.character {
79 c.timer = tick_attack_or_default(data, self.timer, None);
80 }
81 match self.static_data.projectile_body {
82 Body::Object(LaserBeam) => {
83 output_events.emit_local(LocalEvent::CreateOutcome(
85 Outcome::CyclopsCharge {
86 pos: data.pos.0
87 + *data.ori.look_dir() * (data.body.max_radius()),
88 },
89 ));
90 },
91 Body::Object(GrenadeClay) => {
92 output_events.emit_local(LocalEvent::CreateOutcome(
94 Outcome::FuseCharge {
95 pos: data.pos.0
96 + *data.ori.look_dir() * (2.5 * data.body.max_radius()),
97 },
98 ));
99 },
100 Body::Object(LaserBeamSmall) => {
101 output_events.emit_local(LocalEvent::CreateOutcome(
102 Outcome::TerracottaStatueCharge {
103 pos: data.pos.0
104 + *data.ori.look_dir() * (data.body.max_radius()),
105 },
106 ));
107 },
108 _ => {},
109 }
110 } else {
111 if let CharacterState::BasicRanged(c) = &mut update.character {
113 c.timer = Duration::default();
114 c.stage_section = StageSection::Recover;
115 c.movement_modifier = c.static_data.movement_modifier.recover;
116 c.ori_modifier = c.static_data.ori_modifier.recover;
117 }
118 }
119 },
120 StageSection::Recover => {
121 if !self.exhausted {
122 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
124 let projectile = self.static_data.projectile.clone().create_projectile(
125 Some(*data.uid),
126 precision_mult,
127 Some(self.static_data.ability_info),
128 );
129 let num_projectiles = self
131 .static_data
132 .num_projectiles
133 .compute(data.heads.map_or(1, |heads| heads.amount() as u32));
134
135 let mut rng = rng();
136
137 let aim_dir = if self.static_data.ori_modifier.buildup.is_some() {
138 data.inputs.look_dir.merge_z(data.ori.look_dir())
139 } else {
140 data.inputs.look_dir
141 };
142
143 let body_offsets = data
145 .body
146 .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
147 let pos = Pos(data.pos.0 + body_offsets);
148
149 let aim_dir = if self.static_data.auto_aim
150 && let Some(sel_pos) = self
151 .static_data
152 .ability_info
153 .input_attr
154 .and_then(|ia| ia.select_pos)
155 {
156 if let Some(ideal_dir) =
157 aim_projectile(self.static_data.projectile_speed, pos.0, sel_pos, true)
158 {
159 ideal_dir
160 } else {
161 aim_dir
162 }
163 } else {
164 aim_dir
165 };
166
167 let dirs = if let Some(spread) = self.static_data.projectile_spread {
168 Either::Left(spread.compute_directions(
169 aim_dir,
170 *data.ori,
171 num_projectiles,
172 &mut rng,
173 ))
174 } else {
175 Either::Right((0..num_projectiles).map(|_| aim_dir))
176 };
177
178 for dir in dirs {
179 output_events.emit_server(ShootEvent {
181 entity: Some(data.entity),
182 source_vel: Some(*data.vel),
183 pos,
184 dir,
185 body: self.static_data.projectile_body,
186 projectile: projectile.clone(),
187 light: self.static_data.projectile_light,
188 speed: self.static_data.projectile_speed,
189 object: None,
190 marker: None,
191 });
192 }
193
194 if let CharacterState::BasicRanged(c) = &mut update.character {
195 c.exhausted = true;
196 }
197 } else if self.timer < self.static_data.recover_duration {
198 if let CharacterState::BasicRanged(c) = &mut update.character {
200 c.timer = tick_attack_or_default(
201 data,
202 self.timer,
203 Some(data.stats.recovery_speed_modifier),
204 );
205 }
206 } else {
207 if input_is_pressed(data, self.static_data.ability_info.input) {
209 reset_state(self, data, output_events, &mut update);
210 } else {
211 end_ability(data, &mut update);
212 }
213 }
214 },
215 _ => {
216 end_ability(data, &mut update);
218 },
219 }
220
221 handle_interrupts(data, &mut update, output_events);
223
224 update
225 }
226}
227
228fn reset_state(
229 data: &Data,
230 join: &JoinData,
231 output_events: &mut OutputEvents,
232 update: &mut StateUpdate,
233) {
234 handle_input(
235 join,
236 output_events,
237 update,
238 data.static_data.ability_info.input,
239 );
240}