veloren_common/states/
leap_shockwave.rs

1use crate::{
2    Explosion, KnockbackDir, RadiusEffect,
3    combat::{
4        self, Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage,
5        DamageKind, GroupTarget, Knockback,
6    },
7    comp::{
8        CharacterState, StateUpdate, ability::Dodgeable, character_state::OutputEvents,
9        item::Reagent, shockwave,
10    },
11    event::{ExplosionEvent, LocalEvent, ShockwaveEvent},
12    outcome::Outcome,
13    states::{
14        behavior::{CharacterBehavior, JoinData},
15        utils::{StageSection, *},
16    },
17};
18use serde::{Deserialize, Serialize};
19use std::time::Duration;
20
21/// Separated out to condense update portions of character state
22#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
23pub struct StaticData {
24    /// How long the state is moving
25    pub movement_duration: Duration,
26    /// How long until state should deal damage
27    pub buildup_duration: Duration,
28    /// How long the state is swinging for
29    pub swing_duration: Duration,
30    /// How long the state has until exiting
31    pub recover_duration: Duration,
32    /// Base damage
33    pub damage: f32,
34    /// Base poise damage
35    pub poise_damage: f32,
36    /// Knockback
37    pub knockback: Knockback,
38    /// Angle of the shockwave
39    pub shockwave_angle: f32,
40    /// Vertical angle of the shockwave
41    pub shockwave_vertical_angle: f32,
42    /// Speed of the shockwave
43    pub shockwave_speed: f32,
44    /// How long the shockwave travels for
45    pub shockwave_duration: Duration,
46    /// If the shockwave can be dodged, and in what way
47    pub dodgeable: Dodgeable,
48    /// Movement speed efficiency
49    pub move_efficiency: f32,
50    /// Adds an effect onto the main damage of the attack
51    pub damage_effect: Option<CombatEffect>,
52    /// What kind of damage the attack does
53    pub damage_kind: DamageKind,
54    /// Used to specify the shockwave to the frontend
55    pub specifier: shockwave::FrontendSpecifier,
56    /// Affects how far forward the player leaps
57    pub forward_leap_strength: f32,
58    /// Affects how high the player leaps
59    pub vertical_leap_strength: f32,
60    /// What key is used to press ability
61    pub ability_info: AbilityInfo,
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
65pub struct Data {
66    /// Struct containing data that does not change over the course of the
67    /// character state
68    pub static_data: StaticData,
69    /// Timer for each stage
70    pub timer: Duration,
71    /// What section the character stage is in
72    pub stage_section: StageSection,
73    /// Whether the attack can deal more damage
74    pub exhausted: bool,
75}
76
77impl CharacterBehavior for Data {
78    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
79        let mut update = StateUpdate::from(data);
80
81        handle_orientation(data, &mut update, 1.0, None);
82
83        match self.stage_section {
84            // Delay before leaping into the air
85            StageSection::Buildup => {
86                handle_move(data, &mut update, 0.3);
87                handle_jump(data, output_events, &mut update, 1.0);
88                // Wait for `buildup_duration` to expire
89                if self.timer < self.static_data.buildup_duration {
90                    if let CharacterState::LeapShockwave(c) = &mut update.character {
91                        c.timer = tick_attack_or_default(data, self.timer, None);
92                    }
93                } else {
94                    // Transitions to leap portion of state after buildup delay
95                    if let CharacterState::LeapShockwave(c) = &mut update.character {
96                        c.timer = Duration::default();
97                        c.stage_section = StageSection::Movement;
98                    }
99                }
100            },
101            StageSection::Movement => {
102                if self.timer < self.static_data.movement_duration {
103                    // Apply jumping force
104                    let progress = 1.0
105                        - self.timer.as_secs_f32()
106                            / self.static_data.movement_duration.as_secs_f32();
107                    handle_forced_movement(data, &mut update, ForcedMovement::Leap {
108                        vertical: self.static_data.vertical_leap_strength,
109                        forward: self.static_data.forward_leap_strength,
110                        progress,
111                        direction: MovementDirection::Look,
112                    });
113
114                    // Increment duration
115                    // If we were to set a timeout for state, this would be
116                    // outside if block and have else check for > movement
117                    // duration * some multiplier
118                    if let CharacterState::LeapShockwave(c) = &mut update.character {
119                        c.timer = tick_attack_or_default(data, self.timer, None);
120                    }
121                } else if data.physics.on_ground.is_some() | data.physics.in_liquid().is_some() {
122                    // Transitions to swing portion of state upon hitting ground
123                    if let CharacterState::LeapShockwave(c) = &mut update.character {
124                        c.timer = Duration::default();
125                        c.stage_section = StageSection::Action;
126                    }
127                }
128            },
129            StageSection::Action => {
130                handle_move(data, &mut update, 0.3);
131                handle_jump(data, output_events, &mut update, 1.0);
132                if self.timer < self.static_data.swing_duration {
133                    // Swings
134                    if let CharacterState::LeapShockwave(c) = &mut update.character {
135                        c.timer = tick_attack_or_default(data, self.timer, None);
136                    }
137
138                    // Attack
139                    let poise = AttackEffect::new(
140                        Some(GroupTarget::OutOfGroup),
141                        CombatEffect::Poise(self.static_data.poise_damage),
142                    )
143                    .with_requirement(CombatRequirement::AnyDamage);
144                    let knockback = AttackEffect::new(
145                        Some(GroupTarget::OutOfGroup),
146                        CombatEffect::Knockback(self.static_data.knockback),
147                    )
148                    .with_requirement(CombatRequirement::AnyDamage);
149                    let mut damage = AttackDamage::new(
150                        Damage {
151                            kind: self.static_data.damage_kind,
152                            value: self.static_data.damage,
153                        },
154                        Some(GroupTarget::OutOfGroup),
155                        rand::random(),
156                    );
157                    if let Some(effect) = &self.static_data.damage_effect {
158                        damage = damage.with_effect(effect.clone());
159                    }
160                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
161                    let attack = Attack::new(Some(self.static_data.ability_info))
162                        .with_damage(damage)
163                        .with_precision(
164                            precision_mult
165                                * self
166                                    .static_data
167                                    .ability_info
168                                    .ability_meta
169                                    .precision_power_mult
170                                    .unwrap_or(1.0),
171                        )
172                        .with_effect(poise)
173                        .with_effect(knockback)
174                        .with_combo_increment();
175                    let properties = shockwave::Properties {
176                        angle: self.static_data.shockwave_angle,
177                        vertical_angle: self.static_data.shockwave_vertical_angle,
178                        speed: self.static_data.shockwave_speed,
179                        duration: self.static_data.shockwave_duration,
180                        attack,
181                        dodgeable: self.static_data.dodgeable,
182                        owner: Some(*data.uid),
183                        specifier: self.static_data.specifier,
184                    };
185                    output_events.emit_server(ShockwaveEvent {
186                        properties,
187                        pos: *data.pos,
188                        ori: *data.ori,
189                    });
190                    // Send local event used for frontend shenanigans
191                    match self.static_data.specifier {
192                        // TODO: Remove this, attacks should never be added this way
193                        shockwave::FrontendSpecifier::IceSpikes => {
194                            let damage = AttackDamage::new(
195                                Damage {
196                                    kind: self.static_data.damage_kind,
197                                    value: self.static_data.damage / 2.,
198                                },
199                                Some(GroupTarget::OutOfGroup),
200                                rand::random(),
201                            );
202                            let attack = Attack::new(Some(self.static_data.ability_info))
203                                .with_damage(damage)
204                                .with_effect(AttackEffect::new(
205                                    Some(GroupTarget::OutOfGroup),
206                                    CombatEffect::Knockback(Knockback {
207                                        direction: KnockbackDir::Away,
208                                        strength: 10.,
209                                    }),
210                                ));
211                            let explosion = Explosion {
212                                effects: vec![RadiusEffect::Attack {
213                                    attack,
214                                    dodgeable: Dodgeable::Roll,
215                                }],
216                                radius: data.body.max_radius() * 3.0,
217                                reagent: Some(Reagent::White),
218                                min_falloff: 0.5,
219                            };
220                            output_events.emit_server(ExplosionEvent {
221                                pos: data.pos.0,
222                                explosion,
223                                owner: Some(*data.uid),
224                            });
225                            output_events.emit_local(LocalEvent::CreateOutcome(
226                                Outcome::IceSpikes {
227                                    pos: data.pos.0
228                                        + *data.ori.look_dir() * (data.body.max_radius()),
229                                },
230                            ));
231                        },
232                        shockwave::FrontendSpecifier::Steam => {
233                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Steam {
234                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
235                            }));
236                        },
237                        _ => {},
238                    };
239                } else {
240                    // Transitions to recover
241                    if let CharacterState::LeapShockwave(c) = &mut update.character {
242                        c.timer = Duration::default();
243                        c.stage_section = StageSection::Recover;
244                    }
245                }
246            },
247            StageSection::Recover => {
248                handle_move(data, &mut update, 0.3);
249                handle_jump(data, output_events, &mut update, 1.0);
250                if self.timer < self.static_data.recover_duration {
251                    // Recovers
252                    if let CharacterState::LeapShockwave(c) = &mut update.character {
253                        c.timer = tick_attack_or_default(
254                            data,
255                            self.timer,
256                            Some(data.stats.recovery_speed_modifier),
257                        );
258                    }
259                } else {
260                    // Done
261                    end_ability(data, &mut update);
262                }
263            },
264            _ => {
265                // If it somehow ends up in an incorrect stage section
266                end_ability(data, &mut update);
267            },
268        }
269
270        // At end of state logic so an interrupt isn't overwritten
271        handle_interrupts(data, &mut update, output_events);
272
273        update
274    }
275}