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