1use crate::{
2 combat,
3 comp::{
4 Body, CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate,
5 character_state::OutputEvents,
6 },
7 event::{EnergyChangeEvent, ShootEvent},
8 states::{
9 behavior::{CharacterBehavior, JoinData},
10 utils::{StageSection, *},
11 },
12 util::Dir,
13};
14use rand::{Rng, rng};
15use serde::{Deserialize, Serialize};
16use std::{f32::consts::TAU, time::Duration};
17
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19pub struct StaticData {
21 pub buildup_duration: Duration,
23 pub shoot_duration: Duration,
25 pub recover_duration: Duration,
27 pub energy_cost: f32,
29 #[serde(default)]
30 pub options: Options,
31 pub projectile: ProjectileConstructor,
33 pub projectile_body: Body,
34 pub projectile_light: Option<LightEmitter>,
35 pub projectile_speed: f32,
36 pub ability_info: AbilityInfo,
38 pub specifier: Option<FrontendSpecifier>,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
43pub struct Options {
44 pub speed_ramp: Option<RampOptions>,
45 pub max_projectiles: Option<u32>,
46 pub offset: Option<OffsetOptions>,
47 #[serde(default)]
48 pub fire_all: bool,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
52pub struct RampOptions {
53 pub max_bonus: f32,
55 pub half_speed_at: u32,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
60pub struct OffsetOptions {
61 pub radius: f32,
62 pub height: f32,
63}
64
65#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
66pub struct Data {
67 pub static_data: StaticData,
70 pub timer: Duration,
72 pub stage_section: StageSection,
74 pub speed: f32,
76 pub projectiles_fired: u32,
78}
79
80impl CharacterBehavior for Data {
81 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
82 let mut update = StateUpdate::from(data);
83 handle_orientation(data, &mut update, 1.0, None);
84 handle_move(data, &mut update, 0.3);
85
86 match self.stage_section {
87 StageSection::Buildup => {
88 if self.timer < self.static_data.buildup_duration {
89 if let CharacterState::RapidRanged(c) = &mut update.character {
91 c.timer = tick_attack_or_default(data, self.timer, None);
92 }
93 } else {
94 if let CharacterState::RapidRanged(c) = &mut update.character {
96 c.timer = Duration::default();
97 c.stage_section = StageSection::Action;
98 }
99 }
100 },
101 StageSection::Action => {
102 let fire_all = self.static_data.options.fire_all
104 && self.static_data.options.max_projectiles.is_some();
105 if self.timer < self.static_data.shoot_duration {
106 if let CharacterState::RapidRanged(c) = &mut update.character {
108 c.timer = self
109 .timer
110 .checked_add(Duration::from_secs_f32(data.dt.0 * self.speed))
111 .unwrap_or_default();
112 }
113 } else if (input_is_pressed(data, self.static_data.ability_info.input) || fire_all)
114 && update.energy.current() >= self.static_data.energy_cost
115 && self
116 .static_data
117 .options
118 .max_projectiles
119 .is_none_or(|max| self.projectiles_fired < max)
120 {
121 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
123 let offset = if let Some(offset) = self.static_data.options.offset {
125 let mut rng = rng();
126 let theta = rng.random::<f32>() * TAU;
127 let radius = offset.radius * rng.random::<f32>().sqrt();
128 let x = radius * theta.sin();
129 let y = radius * theta.cos();
130 let z = offset.height;
131 vek::Vec3::new(x, y, z)
132 } else {
133 data.body.projectile_offsets(
134 update.ori.look_vec(),
135 data.scale.map_or(1.0, |s| s.0),
136 )
137 };
138 let pos = Pos(data.pos.0 + offset);
139
140 let direction: Dir = if self.static_data.projectile_speed < 1.0 {
141 Dir::down()
142 } else {
143 data.inputs.look_dir
144 };
145
146 let projectile = self.static_data.projectile.clone().create_projectile(
147 Some(*data.uid),
148 precision_mult,
149 Some(self.static_data.ability_info),
150 );
151 output_events.emit_server(ShootEvent {
152 entity: Some(data.entity),
153 source_vel: Some(*data.vel),
154 pos,
155 dir: direction,
156 body: self.static_data.projectile_body,
157 projectile,
158 light: self.static_data.projectile_light,
159 speed: self.static_data.projectile_speed,
160 object: None,
161 marker: None,
162 });
163
164 output_events.emit_server(EnergyChangeEvent {
166 entity: data.entity,
167 change: -self.static_data.energy_cost,
168 reset_rate: false,
169 });
170
171 let new_speed = if let Some(speed_ramp) = self.static_data.options.speed_ramp {
174 1.0 + self.projectiles_fired as f32
175 / (speed_ramp.half_speed_at as f32 + self.projectiles_fired as f32)
176 * speed_ramp.max_bonus
177 } else {
178 1.0
179 };
180
181 if let CharacterState::RapidRanged(c) = &mut update.character {
182 c.timer = Duration::default();
183 c.speed = new_speed;
184 c.projectiles_fired = self.projectiles_fired + 1;
185 }
186 } else {
187 if let CharacterState::RapidRanged(c) = &mut update.character {
189 c.timer = Duration::default();
190 c.stage_section = StageSection::Recover;
191 }
192 }
193 },
194 StageSection::Recover => {
195 if self.timer < self.static_data.recover_duration {
196 if let CharacterState::RapidRanged(c) = &mut update.character {
198 c.timer = tick_attack_or_default(
199 data,
200 self.timer,
201 Some(data.stats.recovery_speed_modifier),
202 );
203 }
204 } else {
205 end_ability(data, &mut update);
207 }
208 },
209 _ => {
210 end_ability(data, &mut update);
212 },
213 }
214
215 handle_interrupts(data, &mut update, output_events);
217
218 update
219 }
220}
221
222#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
223pub enum FrontendSpecifier {
224 FireRainPhoenix,
225}