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, 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)]
19/// Separated out to condense update portions of character state
20pub struct StaticData {
21    /// How long we've readied the weapon
22    pub buildup_duration: Duration,
23    /// How long the state is shooting
24    pub shoot_duration: Duration,
25    /// How long the state has until exiting
26    pub recover_duration: Duration,
27    /// Energy cost per projectile
28    pub energy_cost: f32,
29    #[serde(default)]
30    pub options: Options,
31    /// Projectile options
32    pub projectile: ProjectileConstructor,
33    pub projectile_body: Body,
34    pub projectile_light: Option<LightEmitter>,
35    pub projectile_speed: f32,
36    /// What key is used to press ability
37    pub ability_info: AbilityInfo,
38    /// Used to specify the attack to the frontend
39    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    /// Max bonus to speed that can be reached
54    pub max_bonus: f32,
55    /// Projectiles required to reach half of max speed
56    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    /// Struct containing data that does not change over the course of the
68    /// character state
69    pub static_data: StaticData,
70    /// Timer for each stage
71    pub timer: Duration,
72    /// What section the character stage is in
73    pub stage_section: StageSection,
74    /// Speed of the state while in shoot section
75    pub speed: f32,
76    /// Number of projectiles fired so far
77    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                    // Buildup to attack
90                    if let CharacterState::RapidRanged(c) = &mut update.character {
91                        c.timer = tick_attack_or_default(data, self.timer, None);
92                    }
93                } else {
94                    // Transition to shoot
95                    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                // We want to ensure that we only "fire all" if there is a finite amount to fire
103                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                    // Draw projectile
107                    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                    // Fire if input is pressed still
122                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
123                    // Gets offsets
124                    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                    // Removes energy from character when arrow is fired
165                    output_events.emit_server(EnergyChangeEvent {
166                        entity: data.entity,
167                        change: -self.static_data.energy_cost,
168                        reset_rate: false,
169                    });
170
171                    // Sets new speed of shoot. Scales based off of the number of projectiles fired
172                    // if there is a speed ramp.
173                    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                    // Transition to recover
188                    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                    // Recover from attack
197                    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                    // Done
206                    end_ability(data, &mut update);
207                }
208            },
209            _ => {
210                // If it somehow ends up in an incorrect stage section
211                end_ability(data, &mut update);
212            },
213        }
214
215        // At end of state logic so an interrupt isn't overwritten
216        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}