Skip to main content

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, marker) =
140                        self.static_data.projectile.clone().create_projectile(
141                            Some(*data.uid),
142                            precision_mult,
143                            Some(self.static_data.ability_info),
144                            Some(data.stats),
145                        );
146                    // Shoots all projectiles simultaneously
147                    let num_projectiles = self
148                        .static_data
149                        .num_projectiles
150                        .compute(data.heads.map_or(1, |heads| heads.amount() as u32));
151
152                    let mut rng = rng();
153
154                    let aim_dir = if self.static_data.ori_modifier.buildup.is_some() {
155                        data.inputs.look_dir.merge_z(data.ori.look_dir())
156                    } else {
157                        data.inputs.look_dir
158                    };
159
160                    // Adds the vertical angle offset if present.
161                    // Unwrap clause fires if the cross product is degenerate.
162                    let aim_dir = if self.static_data.vertical_angle_offset != 0.0 {
163                        let cross = vek::Vec3::unit_z().cross(*aim_dir).normalized();
164                        Dir::from_unnormalized(
165                            vek::Quaternion::rotation_3d(
166                                -self.static_data.vertical_angle_offset,
167                                cross,
168                            ) * *aim_dir,
169                        )
170                        .unwrap_or(aim_dir)
171                    } else {
172                        aim_dir
173                    };
174
175                    // Gets offsets
176                    let body_offsets = data
177                        .body
178                        .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
179                    let pos = Pos(data.pos.0 + body_offsets);
180
181                    let aim_dir = if self.static_data.auto_aim
182                        && let Some(sel_pos) = self
183                            .static_data
184                            .ability_info
185                            .input_attr
186                            .and_then(|ia| ia.select_pos)
187                    {
188                        if let Some(ideal_dir) =
189                            aim_projectile(self.static_data.projectile_speed, pos.0, sel_pos, true)
190                        {
191                            ideal_dir
192                        } else {
193                            aim_dir
194                        }
195                    } else {
196                        aim_dir
197                    };
198
199                    let dirs = if let Some(spread) = self.static_data.projectile_spread {
200                        Either::Left(spread.compute_directions(
201                            aim_dir,
202                            *data.ori,
203                            num_projectiles,
204                            &mut rng,
205                        ))
206                    } else {
207                        Either::Right((0..num_projectiles).map(|_| aim_dir))
208                    };
209
210                    for dir in dirs {
211                        // Tells server to create and shoot the projectile
212                        output_events.emit_server(ShootEvent {
213                            entity: Some(data.entity),
214                            source_vel: Some(*data.vel),
215                            pos,
216                            dir,
217                            body: self.static_data.projectile_body,
218                            projectile: projectile.clone(),
219                            light: self.static_data.projectile_light,
220                            speed: self.static_data.projectile_speed
221                                * data.stats.projectile_speed_mult,
222                            object: None,
223                            marker: self.static_data.marker.or(marker),
224                        });
225                    }
226
227                    if let CharacterState::BasicRanged(c) = &mut update.character {
228                        c.exhausted = true;
229                    }
230                } else if self.timer < self.static_data.recover_duration {
231                    // Recovers
232                    if let CharacterState::BasicRanged(c) = &mut update.character {
233                        c.timer = tick_attack_or_default(data, self.timer, None);
234                    }
235                } else {
236                    // Done
237                    if input_is_pressed(data, self.static_data.ability_info.input) {
238                        reset_state(self, data, output_events, &mut update);
239                    } else {
240                        end_ability(data, &mut update);
241                    }
242                }
243            },
244            _ => {
245                // If it somehow ends up in an incorrect stage section
246                end_ability(data, &mut update);
247            },
248        }
249
250        // At end of state logic so an interrupt isn't overwritten
251        handle_interrupts(data, &mut update, output_events);
252
253        update
254    }
255}
256
257fn reset_state(
258    data: &Data,
259    join: &JoinData,
260    output_events: &mut OutputEvents,
261    update: &mut StateUpdate,
262) {
263    handle_input(
264        join,
265        output_events,
266        update,
267        data.static_data.ability_info.input,
268    );
269}