veloren_common/states/
basic_ranged.rs

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