veloren_common/states/
rapid_ranged.rs

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)]
22/// Separated out to condense update portions of character state
23pub struct StaticData {
24    /// How long we've readied the weapon
25    pub buildup_duration: Duration,
26    /// How long the state is shooting
27    pub shoot_duration: Duration,
28    /// How long the state has until exiting
29    pub recover_duration: Duration,
30    /// Energy cost per projectile
31    pub energy_cost: f32,
32    #[serde(default)]
33    pub options: Options,
34    /// Projectile options
35    pub projectile: ProjectileConstructor,
36    pub projectile_body: Body,
37    pub projectile_light: Option<LightEmitter>,
38    pub projectile_speed: f32,
39    /// What key is used to press ability
40    pub ability_info: AbilityInfo,
41    /// Used to specify the attack to the frontend
42    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    /// Max bonus to speed that can be reached
57    pub max_bonus: f32,
58    /// Projectiles required to reach half of max speed
59    pub half_speed_at: u32,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
63pub struct OffsetOptions {
64    //Radius adds random spawn location within a circle parallel to XY plane; 0 results in no
65    // variation
66    pub radius: f32,
67    pub height: f32,
68    #[serde(default)]
69    //If the projectiles should converge on users aim direction
70    pub converge: bool,
71}
72
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74pub struct Data {
75    /// Struct containing data that does not change over the course of the
76    /// character state
77    pub static_data: StaticData,
78    /// Timer for each stage
79    pub timer: Duration,
80    /// What section the character stage is in
81    pub stage_section: StageSection,
82    /// Speed of the state while in shoot section
83    pub speed: f32,
84    /// Number of projectiles fired so far
85    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                    // Buildup to attack
98                    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                    // Transition to shoot
112                    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                // We want to ensure that we only "fire all" if there is a finite amount to fire
120                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                    // Draw projectile
124                    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                    // Fire if input is pressed still
139                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
140                    // Gets offsets
141                    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                    // Removes energy from character when arrow is fired
211                    output_events.emit_server(EnergyChangeEvent {
212                        entity: data.entity,
213                        change: -self.static_data.energy_cost,
214                        reset_rate: false,
215                    });
216
217                    // Sets new speed of shoot. Scales based off of the number of projectiles fired
218                    // if there is a speed ramp.
219                    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                    // Transition to recover
234                    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                    // Recover from attack
243                    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                    // Done
252                    end_ability(data, &mut update);
253                }
254            },
255            _ => {
256                // If it somehow ends up in an incorrect stage section
257                end_ability(data, &mut update);
258            },
259        }
260
261        // At end of state logic so an interrupt isn't overwritten
262        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    //The height of the implosion effect along with the initial radius of the implosion sphere
272    PyroclasmCharge { height: f32, radius: f32 },
273}