veloren_common/states/
basic_beam.rs

1use crate::{
2    combat::{
3        self, Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage,
4        DamageKind, DamageSource, GroupTarget,
5    },
6    comp::{
7        Body, CharacterState, Ori, StateUpdate, beam,
8        body::{biped_large, bird_large, golem},
9        character_state::OutputEvents,
10        object::Body::{Flamethrower, Lavathrower},
11    },
12    event::LocalEvent,
13    outcome::Outcome,
14    resources::Secs,
15    states::{
16        behavior::{CharacterBehavior, JoinData},
17        utils::*,
18    },
19    terrain::Block,
20    util::Dir,
21};
22use hashbrown::HashMap;
23use serde::{Deserialize, Serialize};
24use std::time::Duration;
25use vek::*;
26
27/// Separated out to condense update portions of character state
28#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct StaticData {
30    /// How long until state should deal damage or heal
31    pub buildup_duration: Duration,
32    /// How long the state has until exiting
33    pub recover_duration: Duration,
34    /// Time required for beam to travel from start pos to end pos
35    pub beam_duration: Secs,
36    /// Base damage per tick
37    pub damage: f32,
38    /// Ticks per second
39    pub tick_rate: f32,
40    /// Max range
41    pub range: f32,
42    /// The radius at the far distance of the beam. Radius linearly increases
43    /// from 0 moving from start pos to end po.
44    pub end_radius: f32,
45    /// Adds an effect onto the main damage of the attack
46    pub damage_effect: Option<CombatEffect>,
47    /// Energy regenerated per tick
48    pub energy_regen: f32,
49    /// Energy drained per second
50    pub energy_drain: f32,
51    /// How fast enemy can rotate with beam
52    pub ori_rate: f32,
53    /// What key is used to press ability
54    pub ability_info: AbilityInfo,
55    /// Used to specify the beam to the frontend
56    pub specifier: beam::FrontendSpecifier,
57}
58
59#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct Data {
61    /// Struct containing data that does not change over the course of the
62    /// character state
63    pub static_data: StaticData,
64    /// Timer for each stage
65    pub timer: Duration,
66    /// What section the character stage is in
67    pub stage_section: StageSection,
68    /// Direction that beam should be aimed in
69    pub aim_dir: Dir,
70    /// Offset for beam start pos
71    pub beam_offset: Vec3<f32>,
72}
73
74impl CharacterBehavior for Data {
75    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
76        let mut update = StateUpdate::from(data);
77
78        let ori_rate = self.static_data.ori_rate;
79
80        handle_orientation(data, &mut update, ori_rate, None);
81        handle_move(data, &mut update, 0.4);
82        handle_jump(data, output_events, &mut update, 1.0);
83
84        // Velocity relative to the current ground
85        let rel_vel = data.vel.0 - data.physics.ground_vel;
86        // Gets offsets
87        let body_offsets = beam_offsets(
88            data.body,
89            data.inputs.look_dir,
90            update.ori.look_vec(),
91            rel_vel,
92            data.physics.on_ground,
93        );
94
95        match self.stage_section {
96            StageSection::Buildup => {
97                if self.timer < self.static_data.buildup_duration {
98                    // Build up
99                    update.character = CharacterState::BasicBeam(Data {
100                        timer: tick_attack_or_default(data, self.timer, None),
101                        ..*self
102                    });
103                    if matches!(data.body, Body::Object(Flamethrower | Lavathrower)) {
104                        // Send local event used for frontend shenanigans
105                        output_events.emit_local(LocalEvent::CreateOutcome(
106                            Outcome::FlamethrowerCharge {
107                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
108                            },
109                        ));
110                    }
111                } else {
112                    let attack = {
113                        let energy = AttackEffect::new(
114                            None,
115                            CombatEffect::EnergyReward(self.static_data.energy_regen),
116                        )
117                        .with_requirement(CombatRequirement::AnyDamage);
118                        let mut damage = AttackDamage::new(
119                            Damage {
120                                source: DamageSource::Energy,
121                                kind: DamageKind::Energy,
122                                value: self.static_data.damage,
123                            },
124                            Some(GroupTarget::OutOfGroup),
125                            rand::random(),
126                        );
127                        if let Some(effect) = self.static_data.damage_effect {
128                            damage = damage.with_effect(effect);
129                        }
130                        let precision_mult =
131                            combat::compute_precision_mult(data.inventory, data.msm);
132                        Attack::default()
133                            .with_damage(damage)
134                            .with_precision(precision_mult)
135                            .with_effect(energy)
136                            .with_combo_increment()
137                    };
138
139                    // Creates beam
140                    data.updater.insert(data.entity, beam::Beam {
141                        attack,
142                        end_radius: self.static_data.end_radius,
143                        range: self.static_data.range,
144                        duration: self.static_data.beam_duration,
145                        tick_dur: Secs(1.0 / self.static_data.tick_rate as f64),
146                        hit_entities: Vec::new(),
147                        hit_durations: HashMap::new(),
148                        specifier: self.static_data.specifier,
149                        bezier: QuadraticBezier3 {
150                            start: data.pos.0 + body_offsets,
151                            ctrl: data.pos.0 + body_offsets,
152                            end: data.pos.0 + body_offsets,
153                        },
154                    });
155                    // Build up
156                    update.character = CharacterState::BasicBeam(Data {
157                        beam_offset: body_offsets,
158                        timer: Duration::default(),
159                        stage_section: StageSection::Action,
160                        ..*self
161                    });
162                }
163            },
164            StageSection::Action => {
165                if input_is_pressed(data, self.static_data.ability_info.input)
166                    && (self.static_data.energy_drain <= f32::EPSILON
167                        || update.energy.current() > 0.0)
168                {
169                    let beam_ori = {
170                        // We want Beam to use Ori of owner.
171                        // But we also want beam to use Z part of where owner looks.
172                        // This means that we need to merge this data to one Ori.
173                        //
174                        // This code just gets look_dir without Z part
175                        // and normalizes it. This is what `xy_dir is`.
176                        //
177                        // Then we find rotation between xy_dir and look_dir
178                        // which gives us quaternion how of what rotation we need
179                        // to do to get Z part we want.
180                        //
181                        // Then we construct Ori without Z part
182                        // and applying `pitch` to get needed orientation.
183                        let look_dir = data.inputs.look_dir;
184                        let xy_dir = Dir::from_unnormalized(Vec3::new(look_dir.x, look_dir.y, 0.0))
185                            .unwrap_or_default();
186                        let pitch = xy_dir.rotation_between(look_dir);
187
188                        Ori::from(Vec3::new(
189                            update.ori.look_vec().x,
190                            update.ori.look_vec().y,
191                            0.0,
192                        ))
193                        .prerotated(pitch)
194                    };
195
196                    update.character = CharacterState::BasicBeam(Data {
197                        beam_offset: body_offsets,
198                        aim_dir: beam_ori.look_dir(),
199                        timer: tick_attack_or_default(data, self.timer, None),
200                        ..*self
201                    });
202
203                    // Consumes energy if there's enough left and ability key is held down
204                    update
205                        .energy
206                        .change_by(-self.static_data.energy_drain * data.dt.0);
207                } else {
208                    update.character = CharacterState::BasicBeam(Data {
209                        timer: Duration::default(),
210                        stage_section: StageSection::Recover,
211                        ..*self
212                    });
213                }
214            },
215            StageSection::Recover => {
216                if self.timer < self.static_data.recover_duration {
217                    update.character = CharacterState::BasicBeam(Data {
218                        timer: tick_attack_or_default(
219                            data,
220                            self.timer,
221                            Some(data.stats.recovery_speed_modifier),
222                        ),
223                        ..*self
224                    });
225                } else {
226                    // Done
227                    end_ability(data, &mut update);
228                    // Make sure attack component is removed
229                    data.updater.remove::<beam::Beam>(data.entity);
230                }
231            },
232            _ => {
233                // If it somehow ends up in an incorrect stage section
234                end_ability(data, &mut update);
235                // Make sure attack component is removed
236                data.updater.remove::<beam::Beam>(data.entity);
237            },
238        }
239
240        // At end of state logic so an interrupt isn't overwritten
241        handle_interrupts(data, &mut update, output_events);
242
243        update
244    }
245}
246
247fn height_offset(body: &Body, look_dir: Dir, velocity: Vec3<f32>, on_ground: Option<Block>) -> f32 {
248    match body {
249        // Hack to make the beam offset correspond to the animation
250        Body::BirdLarge(b) => {
251            let height_factor = match b.species {
252                bird_large::Species::Phoenix => 0.5,
253                bird_large::Species::Cockatrice => 0.4,
254                _ => 0.3,
255            };
256            body.height() * height_factor
257                + if on_ground.is_none() {
258                    (2.0 - velocity.xy().magnitude() * 0.25).max(-1.0)
259                } else {
260                    0.0
261                }
262        },
263        Body::Golem(b) => {
264            let height_factor = match b.species {
265                golem::Species::Mogwai => 0.4,
266                _ => 0.9,
267            };
268            const DIR_COEFF: f32 = 2.0;
269            body.height() * height_factor + look_dir.z * DIR_COEFF
270        },
271        Body::BipedLarge(b) => match b.species {
272            biped_large::Species::Mindflayer => body.height() * 0.6,
273            biped_large::Species::SeaBishop => body.height() * 0.4,
274            biped_large::Species::Cursekeeper => body.height() * 0.8,
275            _ => body.height() * 0.5,
276        },
277        _ => body.height() * 0.5,
278    }
279}
280
281pub fn beam_offsets(
282    body: &Body,
283    look_dir: Dir,
284    ori: Vec3<f32>,
285    velocity: Vec3<f32>,
286    on_ground: Option<Block>,
287) -> Vec3<f32> {
288    let dim = body.dimensions();
289    // The width (shoulder to shoulder) and length (nose to tail)
290    let (width, length) = (dim.x, dim.y);
291    let body_radius = if length > width {
292        // Dachshund-like
293        body.max_radius()
294    } else {
295        // Cyclops-like
296        body.min_radius()
297    };
298    let body_offsets_z = height_offset(body, look_dir, velocity, on_ground);
299    Vec3::new(
300        body_radius * ori.x * 1.1,
301        body_radius * ori.y * 1.1,
302        body_offsets_z,
303    )
304}