1use crate::{
2 combat,
3 comp::{
4 Body, CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate,
5 character_state::OutputEvents,
6 },
7 event::{EnergyChangeEvent, LocalEvent, ShootEvent},
8 outcome::Outcome,
9 states::{
10 behavior::{CharacterBehavior, JoinData},
11 utils::{StageSection, *},
12 },
13 terrain::Block,
14 util::Dir,
15 vol::ReadVol,
16};
17use rand::{RngExt, rng};
18use serde::{Deserialize, Serialize};
19use std::{f32::consts::TAU, time::Duration};
20
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22pub struct StaticData {
24 pub buildup_duration: Duration,
26 pub shoot_duration: Duration,
28 pub recover_duration: Duration,
30 pub energy_cost: f32,
32 #[serde(default)]
33 pub options: Options,
34 pub projectile: ProjectileConstructor,
36 pub projectile_body: Body,
37 pub projectile_light: Option<LightEmitter>,
38 pub projectile_speed: f32,
39 pub ability_info: AbilityInfo,
41 pub specifier: Option<FrontendSpecifier>,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
46pub struct Options {
47 pub speed_ramp: Option<RampOptions>,
48 pub max_projectiles: Option<u32>,
49 pub offset: Option<OffsetOptions>,
50 #[serde(default)]
51 pub fire_all: bool,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
55pub struct RampOptions {
56 pub max_bonus: f32,
58 pub half_speed_at: u32,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
63pub struct OffsetOptions {
64 pub radius: f32,
67 pub height: f32,
68 #[serde(default)]
69 pub converge: bool,
71}
72
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74pub struct Data {
75 pub static_data: StaticData,
78 pub timer: Duration,
80 pub stage_section: StageSection,
82 pub speed: f32,
84 pub projectiles_fired: u32,
86}
87
88impl CharacterBehavior for Data {
89 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
90 let mut update = StateUpdate::from(data);
91 handle_orientation(data, &mut update, 1.0, None);
92 handle_move(data, &mut update, 0.3);
93
94 match self.stage_section {
95 StageSection::Buildup => {
96 if self.timer < self.static_data.buildup_duration {
97 if let CharacterState::RapidRanged(c) = &mut update.character {
99 c.timer = tick_attack_or_default(data, self.timer, None);
100 }
101 if matches!(
102 self.static_data.specifier,
103 Some(FrontendSpecifier::PyroclasmCharge { .. })
104 ) && self.timer == Duration::default()
105 {
106 output_events.emit_local(LocalEvent::CreateOutcome(
107 Outcome::PyroclasmCharge { pos: data.pos.0 },
108 ));
109 }
110 } else {
111 if let CharacterState::RapidRanged(c) = &mut update.character {
113 c.timer = Duration::default();
114 c.stage_section = StageSection::Action;
115 }
116 }
117 },
118 StageSection::Action => {
119 let fire_all = self.static_data.options.fire_all
121 && self.static_data.options.max_projectiles.is_some();
122 if self.timer < self.static_data.shoot_duration {
123 if let CharacterState::RapidRanged(c) = &mut update.character {
125 c.timer = self
126 .timer
127 .checked_add(Duration::from_secs_f32(data.dt.0 * self.speed))
128 .unwrap_or_default();
129 }
130 } else if (input_is_pressed(data, self.static_data.ability_info.input) || fire_all)
131 && update.energy.current() >= self.static_data.energy_cost
132 && self
133 .static_data
134 .options
135 .max_projectiles
136 .is_none_or(|max| self.projectiles_fired < max)
137 {
138 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
140 let (pos, direction): (Pos, Dir) =
142 if let Some(offset) = self.static_data.options.offset {
143 let mut rng = rng();
144 let rand_offset = if offset.radius > 0.0 {
145 let theta = rng.random::<f32>() * TAU;
146 let r = offset.radius * rng.random::<f32>().sqrt();
147 vek::Vec3::new(r * theta.sin(), r * theta.cos(), offset.height)
148 } else {
149 vek::Vec3::new(0.0, 0.0, offset.height)
150 };
151
152 if offset.converge {
153 let offset_pos = Pos(data.pos.0 + rand_offset);
154 const MAX_RANGE: f32 = 200.0;
155 let eye_pos = data.pos.0
156 + vek::Vec3::unit_z()
157 * data.body.eye_height(data.scale.map_or(1.0, |s| s.0));
158 let ray_end = eye_pos + *data.inputs.look_dir * MAX_RANGE;
159 let (dist, _) = data
160 .terrain
161 .ray(eye_pos, ray_end)
162 .until(Block::is_solid)
163 .cast();
164 let terrain_point = eye_pos + *data.inputs.look_dir * dist;
165 let projectile_dir =
166 Dir::from_unnormalized(terrain_point - offset_pos.0)
167 .unwrap_or(data.inputs.look_dir);
168 (offset_pos, projectile_dir)
169 } else {
170 let base_pos = Pos(data.pos.0 + rand_offset);
171 let base_dir: Dir = if self.static_data.projectile_speed < 1.0 {
172 Dir::down()
173 } else {
174 data.inputs.look_dir
175 };
176 (base_pos, base_dir)
177 }
178 } else {
179 let body_offset = data.body.projectile_offsets(
180 update.ori.look_vec(),
181 data.scale.map_or(1.0, |s| s.0),
182 );
183 let base_pos = Pos(data.pos.0 + body_offset);
184 let base_dir: Dir = if self.static_data.projectile_speed < 1.0 {
185 Dir::down()
186 } else {
187 data.inputs.look_dir
188 };
189 (base_pos, base_dir)
190 };
191
192 let projectile = self.static_data.projectile.clone().create_projectile(
193 Some(*data.uid),
194 precision_mult,
195 Some(self.static_data.ability_info),
196 );
197 output_events.emit_server(ShootEvent {
198 entity: Some(data.entity),
199 source_vel: Some(*data.vel),
200 pos,
201 dir: direction,
202 body: self.static_data.projectile_body,
203 projectile,
204 light: self.static_data.projectile_light,
205 speed: self.static_data.projectile_speed,
206 object: None,
207 marker: None,
208 });
209
210 output_events.emit_server(EnergyChangeEvent {
212 entity: data.entity,
213 change: -self.static_data.energy_cost,
214 reset_rate: false,
215 });
216
217 let new_speed = if let Some(speed_ramp) = self.static_data.options.speed_ramp {
220 1.0 + self.projectiles_fired as f32
221 / (speed_ramp.half_speed_at as f32 + self.projectiles_fired as f32)
222 * speed_ramp.max_bonus
223 } else {
224 1.0
225 };
226
227 if let CharacterState::RapidRanged(c) = &mut update.character {
228 c.timer = Duration::default();
229 c.speed = new_speed;
230 c.projectiles_fired = self.projectiles_fired + 1;
231 }
232 } else {
233 if let CharacterState::RapidRanged(c) = &mut update.character {
235 c.timer = Duration::default();
236 c.stage_section = StageSection::Recover;
237 }
238 }
239 },
240 StageSection::Recover => {
241 if self.timer < self.static_data.recover_duration {
242 if let CharacterState::RapidRanged(c) = &mut update.character {
244 c.timer = tick_attack_or_default(
245 data,
246 self.timer,
247 Some(data.stats.recovery_speed_modifier),
248 );
249 }
250 } else {
251 end_ability(data, &mut update);
253 }
254 },
255 _ => {
256 end_ability(data, &mut update);
258 },
259 }
260
261 handle_interrupts(data, &mut update, output_events);
263
264 update
265 }
266}
267
268#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
269pub enum FrontendSpecifier {
270 FireRainPhoenix,
271 PyroclasmCharge { height: f32, radius: f32 },
273}