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, 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                    // We want Beam to use Ori of owner.
170                    // But we also want beam to use Z part of where owner looks.
171                    // This means that we need to merge this data to one Ori.
172                    let beam_dir = data.inputs.look_dir.merge_z(data.ori.look_dir());
173
174                    update.character = CharacterState::BasicBeam(Data {
175                        beam_offset: body_offsets,
176                        aim_dir: beam_dir,
177                        timer: tick_attack_or_default(data, self.timer, None),
178                        ..*self
179                    });
180
181                    // Consumes energy if there's enough left and ability key is held down
182                    update
183                        .energy
184                        .change_by(-self.static_data.energy_drain * data.dt.0);
185                } else {
186                    update.character = CharacterState::BasicBeam(Data {
187                        timer: Duration::default(),
188                        stage_section: StageSection::Recover,
189                        ..*self
190                    });
191                }
192            },
193            StageSection::Recover => {
194                if self.timer < self.static_data.recover_duration {
195                    update.character = CharacterState::BasicBeam(Data {
196                        timer: tick_attack_or_default(
197                            data,
198                            self.timer,
199                            Some(data.stats.recovery_speed_modifier),
200                        ),
201                        ..*self
202                    });
203                } else {
204                    // Done
205                    end_ability(data, &mut update);
206                    // Make sure attack component is removed
207                    data.updater.remove::<beam::Beam>(data.entity);
208                }
209            },
210            _ => {
211                // If it somehow ends up in an incorrect stage section
212                end_ability(data, &mut update);
213                // Make sure attack component is removed
214                data.updater.remove::<beam::Beam>(data.entity);
215            },
216        }
217
218        // At end of state logic so an interrupt isn't overwritten
219        handle_interrupts(data, &mut update, output_events);
220
221        update
222    }
223}
224
225fn height_offset(body: &Body, look_dir: Dir, velocity: Vec3<f32>, on_ground: Option<Block>) -> f32 {
226    match body {
227        // Hack to make the beam offset correspond to the animation
228        Body::BirdLarge(b) => {
229            let height_factor = match b.species {
230                bird_large::Species::Phoenix => 0.5,
231                bird_large::Species::Cockatrice => 0.4,
232                _ => 0.3,
233            };
234            body.height() * height_factor
235                + if on_ground.is_none() {
236                    (2.0 - velocity.xy().magnitude() * 0.25).max(-1.0)
237                } else {
238                    0.0
239                }
240        },
241        Body::Golem(b) => {
242            let height_factor = match b.species {
243                golem::Species::Mogwai => 0.4,
244                _ => 0.9,
245            };
246            const DIR_COEFF: f32 = 2.0;
247            body.height() * height_factor + look_dir.z * DIR_COEFF
248        },
249        Body::BipedLarge(b) => match b.species {
250            biped_large::Species::Mindflayer => body.height() * 0.6,
251            biped_large::Species::SeaBishop => body.height() * 0.4,
252            biped_large::Species::Cursekeeper => body.height() * 0.8,
253            _ => body.height() * 0.5,
254        },
255        _ => body.height() * 0.5,
256    }
257}
258
259pub fn beam_offsets(
260    body: &Body,
261    look_dir: Dir,
262    ori: Vec3<f32>,
263    velocity: Vec3<f32>,
264    on_ground: Option<Block>,
265) -> Vec3<f32> {
266    let dim = body.dimensions();
267    // The width (shoulder to shoulder) and length (nose to tail)
268    let (width, length) = (dim.x, dim.y);
269    let body_radius = if length > width {
270        // Dachshund-like
271        body.max_radius()
272    } else {
273        // Cyclops-like
274        body.min_radius()
275    };
276    let body_offsets_z = height_offset(body, look_dir, velocity, on_ground);
277    Vec3::new(
278        body_radius * ori.x * 1.1,
279        body_radius * ori.y * 1.1,
280        body_offsets_z,
281    )
282}