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