veloren_common/states/
basic_ranged.rs

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