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