veloren_common/states/
basic_ranged.rs

1use crate::{
2    combat::{self, CombatEffect},
3    comp::{
4        Body, CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate,
5        ability::Amount,
6        character_state::OutputEvents,
7        object::Body::{GrenadeClay, LaserBeam, LaserBeamSmall},
8    },
9    event::{LocalEvent, ShootEvent},
10    outcome::Outcome,
11    states::{
12        behavior::{CharacterBehavior, JoinData},
13        utils::*,
14    },
15    util::Dir,
16};
17use rand::{Rng, thread_rng};
18use serde::{Deserialize, Serialize};
19use std::time::Duration;
20
21/// Separated out to condense update portions of character state
22#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
23pub struct StaticData {
24    /// How much buildup is required before the attack
25    pub buildup_duration: Duration,
26    /// How long the state has until exiting
27    pub recover_duration: Duration,
28    /// How much spread there is when more than 1 projectile is created
29    pub projectile_spread: f32,
30    /// Projectile variables
31    pub projectile: ProjectileConstructor,
32    pub projectile_body: Body,
33    pub projectile_light: Option<LightEmitter>,
34    pub projectile_speed: f32,
35    /// How many projectiles are simultaneously fired
36    pub num_projectiles: Amount,
37    /// What key is used to press ability
38    pub ability_info: AbilityInfo,
39    pub damage_effect: Option<CombatEffect>,
40    /// Adjusts move speed during the attack per stage
41    pub movement_modifier: MovementModifier,
42    /// Adjusts turning rate during the attack per stage
43    pub ori_modifier: OrientationModifier,
44}
45
46#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
47pub struct Data {
48    /// Struct containing data that does not change over the course of the
49    /// character state
50    pub static_data: StaticData,
51    /// Timer for each stage
52    pub timer: Duration,
53    /// What section the character stage is in
54    pub stage_section: StageSection,
55    /// Whether the attack fired already
56    pub exhausted: bool,
57    /// Adjusts move speed during the attack
58    pub movement_modifier: Option<f32>,
59    /// How fast the entity should turn
60    pub ori_modifier: Option<f32>,
61}
62
63impl CharacterBehavior for Data {
64    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
65        let mut update = StateUpdate::from(data);
66
67        handle_orientation(data, &mut update, self.ori_modifier.unwrap_or(1.0), None);
68        handle_move(data, &mut update, self.movement_modifier.unwrap_or(0.7));
69        handle_jump(data, output_events, &mut update, 1.0);
70
71        match self.stage_section {
72            StageSection::Buildup => {
73                if self.timer < self.static_data.buildup_duration {
74                    // Build up
75                    update.character = CharacterState::BasicRanged(Data {
76                        timer: tick_attack_or_default(data, self.timer, None),
77                        ..*self
78                    });
79                    match self.static_data.projectile_body {
80                        Body::Object(LaserBeam) => {
81                            // Send local event used for frontend shenanigans
82                            output_events.emit_local(LocalEvent::CreateOutcome(
83                                Outcome::CyclopsCharge {
84                                    pos: data.pos.0
85                                        + *data.ori.look_dir() * (data.body.max_radius()),
86                                },
87                            ));
88                        },
89                        Body::Object(GrenadeClay) => {
90                            // Send local event used for frontend shenanigans
91                            output_events.emit_local(LocalEvent::CreateOutcome(
92                                Outcome::FuseCharge {
93                                    pos: data.pos.0
94                                        + *data.ori.look_dir() * (2.5 * data.body.max_radius()),
95                                },
96                            ));
97                        },
98                        Body::Object(LaserBeamSmall) => {
99                            output_events.emit_local(LocalEvent::CreateOutcome(
100                                Outcome::TerracottaStatueCharge {
101                                    pos: data.pos.0
102                                        + *data.ori.look_dir() * (data.body.max_radius()),
103                                },
104                            ));
105                        },
106                        _ => {},
107                    }
108                } else {
109                    // Transitions to recover section of stage
110                    update.character = CharacterState::BasicRanged(Data {
111                        timer: Duration::default(),
112                        stage_section: StageSection::Recover,
113                        movement_modifier: self.static_data.movement_modifier.recover,
114                        ori_modifier: self.static_data.ori_modifier.recover,
115                        ..*self
116                    });
117                }
118            },
119            StageSection::Recover => {
120                if !self.exhausted {
121                    // Fire
122                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
123                    let projectile = self.static_data.projectile.create_projectile(
124                        Some(*data.uid),
125                        precision_mult,
126                        self.static_data.damage_effect,
127                    );
128                    // Shoots all projectiles simultaneously
129                    let num_projectiles = self
130                        .static_data
131                        .num_projectiles
132                        .compute(data.heads.map_or(1, |heads| heads.amount() as u32));
133
134                    for i in 0..num_projectiles {
135                        // Gets offsets
136                        let body_offsets = data.body.projectile_offsets(
137                            update.ori.look_vec(),
138                            data.scale.map_or(1.0, |s| s.0),
139                        );
140                        let pos = Pos(data.pos.0 + body_offsets);
141
142                        let dir = {
143                            let look_dir = if self.static_data.ori_modifier.buildup.is_some() {
144                                data.inputs.look_dir.merge_z(data.ori.look_dir())
145                            } else {
146                                data.inputs.look_dir
147                            };
148
149                            // Adds a slight spread to the projectiles. First projectile has no
150                            // spread, and spread increases linearly
151                            // with number of projectiles created.
152                            Dir::from_unnormalized(look_dir.map(|x| {
153                                let offset = (2.0 * thread_rng().gen::<f32>() - 1.0)
154                                    * self.static_data.projectile_spread
155                                    * i as f32;
156                                x + offset
157                            }))
158                            .unwrap_or(data.inputs.look_dir)
159                        };
160
161                        // Tells server to create and shoot the projectile
162                        output_events.emit_server(ShootEvent {
163                            entity: Some(data.entity),
164                            pos,
165                            dir,
166                            body: self.static_data.projectile_body,
167                            projectile: projectile.clone(),
168                            light: self.static_data.projectile_light,
169                            speed: self.static_data.projectile_speed,
170                            object: None,
171                        });
172                    }
173
174                    update.character = CharacterState::BasicRanged(Data {
175                        exhausted: true,
176                        ..*self
177                    });
178                } else if self.timer < self.static_data.recover_duration {
179                    // Recovers
180                    update.character = CharacterState::BasicRanged(Data {
181                        timer: tick_attack_or_default(
182                            data,
183                            self.timer,
184                            Some(data.stats.recovery_speed_modifier),
185                        ),
186                        ..*self
187                    });
188                } else {
189                    // Done
190                    if input_is_pressed(data, self.static_data.ability_info.input) {
191                        reset_state(self, data, output_events, &mut update);
192                    } else {
193                        end_ability(data, &mut update);
194                    }
195                }
196            },
197            _ => {
198                // If it somehow ends up in an incorrect stage section
199                end_ability(data, &mut update);
200            },
201        }
202
203        // At end of state logic so an interrupt isn't overwritten
204        handle_interrupts(data, &mut update, output_events);
205
206        update
207    }
208}
209
210fn reset_state(
211    data: &Data,
212    join: &JoinData,
213    output_events: &mut OutputEvents,
214    update: &mut StateUpdate,
215) {
216    handle_input(
217        join,
218        output_events,
219        update,
220        data.static_data.ability_info.input,
221    );
222}