veloren_common/states/
shockwave.rs

1use crate::{
2    combat::{
3        self, Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage,
4        DamageKind, DamageSource, GroupTarget, Knockback,
5    },
6    comp::{
7        CharacterState, StateUpdate, ability::Dodgeable, character_state::OutputEvents, shockwave,
8    },
9    event::{LocalEvent, ShockwaveEvent},
10    outcome::Outcome,
11    states::{
12        behavior::{CharacterBehavior, JoinData},
13        utils::*,
14    },
15};
16use serde::{Deserialize, Serialize};
17use std::time::Duration;
18
19/// Separated out to condense update portions of character state
20#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
21pub struct StaticData {
22    /// How long until state should deal damage
23    pub buildup_duration: Duration,
24    /// How long the state is swinging for
25    pub swing_duration: Duration,
26    /// How long the state has until exiting
27    pub recover_duration: Duration,
28    /// Base damage
29    pub damage: f32,
30    /// Base poise damage
31    pub poise_damage: f32,
32    /// Knockback
33    pub knockback: Knockback,
34    /// Angle of the shockwave
35    pub shockwave_angle: f32,
36    /// Vertical angle of the shockwave
37    pub shockwave_vertical_angle: f32,
38    /// Speed of the shockwave
39    pub shockwave_speed: f32,
40    /// How long the shockwave travels for
41    pub shockwave_duration: Duration,
42    /// If the shockwave can be dodged, and in what way
43    pub dodgeable: Dodgeable,
44    /// Movement speed efficiency
45    pub move_efficiency: f32,
46    /// What key is used to press ability
47    pub ability_info: AbilityInfo,
48    /// Adds an effect onto the main damage of the attack
49    pub damage_effect: Option<CombatEffect>,
50    /// What kind of damage the attack does
51    pub damage_kind: DamageKind,
52    /// Used to specify the shockwave to the frontend
53    pub specifier: shockwave::FrontendSpecifier,
54    /// Controls outcome emission
55    pub emit_outcome: bool,
56    /// How fast enemy can rotate
57    pub ori_rate: f32,
58    /// Timing of shockwave
59    pub timing: Timing,
60    pub minimum_combo: Option<u32>,
61    pub combo_on_use: u32,
62    pub combo_consumption: ComboConsumption,
63}
64
65#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
66pub struct Data {
67    /// Struct containing data that does not change over the course of the
68    /// character state
69    pub static_data: StaticData,
70    /// Timer for each stage
71    pub timer: Duration,
72    /// What section the character stage is in
73    pub stage_section: StageSection,
74}
75
76impl CharacterBehavior for Data {
77    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
78        let mut update = StateUpdate::from(data);
79
80        handle_orientation(data, &mut update, self.static_data.ori_rate, None);
81        handle_move(data, &mut update, self.static_data.move_efficiency);
82
83        match self.stage_section {
84            StageSection::Buildup => {
85                if self.timer < self.static_data.buildup_duration {
86                    // Build up
87                    update.character = CharacterState::Shockwave(Data {
88                        timer: tick_attack_or_default(data, self.timer, None),
89                        ..*self
90                    });
91                } else {
92                    // Attack
93                    if matches!(self.static_data.timing, Timing::PostBuildup) {
94                        self.attack(data, output_events);
95                    }
96
97                    // Transitions to swing
98                    update.character = CharacterState::Shockwave(Data {
99                        timer: Duration::default(),
100                        stage_section: StageSection::Action,
101                        ..*self
102                    });
103                }
104            },
105            StageSection::Action => {
106                if self.timer < self.static_data.swing_duration {
107                    // Swings
108                    update.character = CharacterState::Shockwave(Data {
109                        timer: tick_attack_or_default(data, self.timer, None),
110                        ..*self
111                    });
112                    // Send local event used for frontend shenanigans
113                    if self.static_data.emit_outcome {
114                        match self.static_data.specifier {
115                            shockwave::FrontendSpecifier::IceSpikes => {
116                                output_events.emit_local(LocalEvent::CreateOutcome(
117                                    Outcome::FlashFreeze {
118                                        pos: data.pos.0
119                                            + *data.ori.look_dir() * (data.body.max_radius()),
120                                    },
121                                ));
122                            },
123                            shockwave::FrontendSpecifier::Ground => {
124                                output_events.emit_local(LocalEvent::CreateOutcome(
125                                    Outcome::GroundSlam {
126                                        pos: data.pos.0
127                                            + *data.ori.look_dir() * (data.body.max_radius()),
128                                    },
129                                ));
130                            },
131                            shockwave::FrontendSpecifier::Steam => {
132                                output_events.emit_local(LocalEvent::CreateOutcome(
133                                    Outcome::Steam {
134                                        pos: data.pos.0
135                                            + *data.ori.look_dir() * (data.body.max_radius()),
136                                    },
137                                ));
138                            },
139                            shockwave::FrontendSpecifier::Fire => {
140                                output_events.emit_local(LocalEvent::CreateOutcome(
141                                    Outcome::FireShockwave {
142                                        pos: data.pos.0
143                                            + *data.ori.look_dir() * (data.body.max_radius()),
144                                    },
145                                ));
146                            },
147                            shockwave::FrontendSpecifier::FireLow => {
148                                output_events.emit_local(LocalEvent::CreateOutcome(
149                                    Outcome::FireLowShockwave {
150                                        pos: data.pos.0
151                                            + *data.ori.look_dir() * (data.body.max_radius()),
152                                    },
153                                ));
154                            },
155                            _ => {
156                                output_events.emit_local(LocalEvent::CreateOutcome(
157                                    Outcome::Swoosh {
158                                        pos: data.pos.0
159                                            + *data.ori.look_dir() * (data.body.max_radius()),
160                                    },
161                                ));
162                            },
163                        }
164                    }
165                } else {
166                    // Attack
167                    if matches!(self.static_data.timing, Timing::PostAction) {
168                        self.attack(data, output_events);
169                    }
170
171                    // Transitions to recover
172                    update.character = CharacterState::Shockwave(Data {
173                        timer: Duration::default(),
174                        stage_section: StageSection::Recover,
175                        ..*self
176                    });
177                }
178            },
179            StageSection::Recover => {
180                if self.timer < self.static_data.recover_duration {
181                    // Recovers
182                    update.character = CharacterState::Shockwave(Data {
183                        timer: tick_attack_or_default(
184                            data,
185                            self.timer,
186                            Some(data.stats.recovery_speed_modifier),
187                        ),
188                        ..*self
189                    });
190                } else {
191                    // Done
192                    end_ability(data, &mut update);
193                }
194            },
195            _ => {
196                // If it somehow ends up in an incorrect stage section
197                end_ability(data, &mut update);
198            },
199        }
200
201        // At end of state logic so an interrupt isn't overwritten
202        handle_interrupts(data, &mut update, output_events);
203
204        update
205    }
206}
207
208impl Data {
209    fn attack(&self, data: &JoinData, output_events: &mut OutputEvents) {
210        if let Some(min_combo) = self.static_data.minimum_combo {
211            self.static_data
212                .combo_consumption
213                .consume(data, output_events, min_combo);
214        }
215
216        let poise = AttackEffect::new(
217            Some(GroupTarget::OutOfGroup),
218            CombatEffect::Poise(self.static_data.poise_damage),
219        )
220        .with_requirement(CombatRequirement::AnyDamage);
221        let knockback = AttackEffect::new(
222            Some(GroupTarget::OutOfGroup),
223            CombatEffect::Knockback(self.static_data.knockback),
224        )
225        .with_requirement(CombatRequirement::AnyDamage);
226        let mut damage = AttackDamage::new(
227            Damage {
228                source: DamageSource::Shockwave,
229                kind: self.static_data.damage_kind,
230                value: self.static_data.damage,
231            },
232            Some(GroupTarget::OutOfGroup),
233            rand::random(),
234        );
235        if let Some(effect) = self.static_data.damage_effect {
236            damage = damage.with_effect(effect);
237        }
238        let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
239        let attack = Attack::default()
240            .with_damage(damage)
241            .with_precision(precision_mult)
242            .with_effect(poise)
243            .with_effect(knockback)
244            .with_combo_increment();
245        let properties = shockwave::Properties {
246            angle: self.static_data.shockwave_angle,
247            vertical_angle: self.static_data.shockwave_vertical_angle,
248            speed: self.static_data.shockwave_speed,
249            duration: self.static_data.shockwave_duration,
250            attack,
251            dodgeable: self.static_data.dodgeable,
252            owner: Some(*data.uid),
253            specifier: self.static_data.specifier,
254        };
255        output_events.emit_server(ShockwaveEvent {
256            properties,
257            pos: *data.pos,
258            ori: *data.ori,
259        });
260    }
261}
262
263#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
264pub enum Timing {
265    PostBuildup,
266    PostAction,
267}