veloren_common/states/
explosion.rs

1use crate::{
2    Damage, DamageKind, DamageSource, Explosion, GroupTarget, Knockback, RadiusEffect,
3    combat::{Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement},
4    comp::{
5        CharacterState, StateUpdate, ability::Dodgeable, character_state::OutputEvents,
6        item::Reagent,
7    },
8    event::ExplosionEvent,
9    explosion::{ColorPreset, TerrainReplacementPreset},
10    states::{
11        behavior::{CharacterBehavior, JoinData},
12        utils::*,
13    },
14};
15use serde::{Deserialize, Serialize};
16use std::time::Duration;
17use vek::Vec3;
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 casting the explosion for
25    pub action_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: f32,
32    /// Knockback
33    pub knockback: Knockback,
34    /// Range of the explosion
35    pub radius: f32,
36    /// Minimum falloff of explosion strength
37    pub min_falloff: f32,
38    /// If the explosion can be dodged, and in what way
39    #[serde(default)]
40    pub dodgeable: Dodgeable,
41    /// Power and color of terrain destruction
42    pub destroy_terrain: Option<(f32, ColorPreset)>,
43    /// Range and kind of terrain replacement
44    pub replace_terrain: Option<(f32, TerrainReplacementPreset)>,
45    /// Whether the explosion is created at eye height instead of the entity's
46    /// pos
47    #[serde(default)]
48    pub eye_height: bool,
49    /// Controls visual effects
50    pub reagent: Option<Reagent>,
51    /// Adjusts move speed during the attack per stage
52    #[serde(default)]
53    pub movement_modifier: MovementModifier,
54    /// Adjusts turning rate during the attack per stage
55    #[serde(default)]
56    pub ori_modifier: OrientationModifier,
57    /// What key is used to press ability
58    pub ability_info: AbilityInfo,
59}
60
61#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
62pub struct Data {
63    /// Struct containing data that does not change over the course of the
64    /// character state
65    pub static_data: StaticData,
66    /// Timer for each stage
67    pub timer: Duration,
68    /// What section the character stage is in
69    pub stage_section: StageSection,
70    /// Adjusts move speed during the attack
71    pub movement_modifier: Option<f32>,
72    /// How fast the entity should turn
73    pub ori_modifier: Option<f32>,
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.ori_modifier.unwrap_or(1.0), None);
81        handle_move(data, &mut update, self.movement_modifier.unwrap_or(0.7));
82        handle_jump(data, output_events, &mut update, 1.0);
83
84        match self.stage_section {
85            StageSection::Buildup => {
86                if self.timer < self.static_data.buildup_duration {
87                    // Build up
88                    update.character = CharacterState::Explosion(Data {
89                        timer: tick_attack_or_default(data, self.timer, None),
90                        ..*self
91                    });
92                } else {
93                    // Transitions to swing section of stage
94                    update.character = CharacterState::Explosion(Data {
95                        timer: Duration::default(),
96                        stage_section: StageSection::Action,
97                        movement_modifier: self.static_data.movement_modifier.swing,
98                        ori_modifier: self.static_data.ori_modifier.swing,
99                        ..*self
100                    });
101                }
102            },
103            StageSection::Action => {
104                if self.timer < self.static_data.action_duration {
105                    // Swings
106                    update.character = CharacterState::Explosion(Data {
107                        timer: tick_attack_or_default(data, self.timer, None),
108                        ..*self
109                    });
110                } else {
111                    // Create an explosion after action before transitioning to recover
112                    let pos = if self.static_data.eye_height {
113                        data.pos.0
114                            + Vec3::unit_z() * data.body.eye_height(data.scale.map_or(1.0, |s| s.0))
115                    } else {
116                        data.pos.0
117                    };
118
119                    let mut effects = vec![RadiusEffect::Attack {
120                        attack: Attack::default()
121                            .with_damage(AttackDamage::new(
122                                Damage {
123                                    source: DamageSource::Explosion,
124                                    kind: DamageKind::Crushing,
125                                    value: self.static_data.damage,
126                                },
127                                Some(GroupTarget::OutOfGroup),
128                                rand::random(),
129                            ))
130                            .with_effect(
131                                AttackEffect::new(
132                                    Some(GroupTarget::OutOfGroup),
133                                    CombatEffect::Poise(self.static_data.poise),
134                                )
135                                .with_requirement(CombatRequirement::AnyDamage),
136                            )
137                            .with_effect(
138                                AttackEffect::new(
139                                    Some(GroupTarget::OutOfGroup),
140                                    CombatEffect::Knockback(self.static_data.knockback),
141                                )
142                                .with_requirement(CombatRequirement::AnyDamage),
143                            ),
144                        dodgeable: self.static_data.dodgeable,
145                    }];
146
147                    if let Some((power, color)) = self.static_data.destroy_terrain {
148                        effects.push(RadiusEffect::TerrainDestruction(power, color.to_rgb()));
149                    }
150
151                    if let Some((radius, replacement_preset)) = self.static_data.replace_terrain {
152                        effects.push(RadiusEffect::ReplaceTerrain(radius, replacement_preset));
153                    }
154
155                    output_events.emit_server(ExplosionEvent {
156                        pos,
157                        explosion: Explosion {
158                            effects,
159                            radius: self.static_data.radius,
160                            reagent: self.static_data.reagent,
161                            min_falloff: self.static_data.min_falloff,
162                        },
163                        owner: Some(*data.uid),
164                    });
165
166                    // Transitions to recover section of stage
167                    update.character = CharacterState::Explosion(Data {
168                        timer: Duration::default(),
169                        stage_section: StageSection::Recover,
170                        movement_modifier: self.static_data.movement_modifier.recover,
171                        ori_modifier: self.static_data.ori_modifier.recover,
172                        ..*self
173                    });
174                }
175            },
176            StageSection::Recover => {
177                if self.timer < self.static_data.recover_duration {
178                    // Recovery
179                    update.character = CharacterState::Explosion(Data {
180                        timer: tick_attack_or_default(
181                            data,
182                            self.timer,
183                            Some(data.stats.recovery_speed_modifier),
184                        ),
185                        movement_modifier: self.static_data.movement_modifier.recover,
186                        ori_modifier: self.static_data.ori_modifier.recover,
187                        ..*self
188                    });
189                } else {
190                    // Done
191                    if input_is_pressed(data, self.static_data.ability_info.input) {
192                        reset_state(self, data, output_events, &mut update);
193                    } else {
194                        end_ability(data, &mut update);
195                    }
196                }
197            },
198            _ => {
199                // If it somehow ends up in an incorrect stage section
200                end_ability(data, &mut update);
201            },
202        }
203
204        // At end of state logic so an interrupt isn't overwritten
205        handle_interrupts(data, &mut update, output_events);
206
207        update
208    }
209}
210
211fn reset_state(
212    data: &Data,
213    join: &JoinData,
214    output_events: &mut OutputEvents,
215    update: &mut StateUpdate,
216) {
217    handle_input(
218        join,
219        output_events,
220        update,
221        data.static_data.ability_info.input,
222    );
223}