veloren_common/states/
combo_melee2.rs

1use crate::{
2    Explosion, RadiusEffect, combat,
3    combat::{Attack, AttackDamage, Damage, DamageKind::Crushing, DamageSource, GroupTarget},
4    comp::{
5        CharacterState, MeleeConstructor, StateUpdate, character_state::OutputEvents,
6        item::Reagent, melee::CustomCombo, tool::Stats,
7    },
8    event::{ExplosionEvent, LocalEvent},
9    outcome::Outcome,
10    states::{
11        behavior::{CharacterBehavior, JoinData},
12        combo_melee2,
13        utils::*,
14    },
15};
16use serde::{Deserialize, Serialize};
17use std::time::Duration;
18use vek::*;
19
20#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct Strike<T> {
23    /// Used to construct the Melee attack
24    pub melee_constructor: MeleeConstructor,
25    /// Initial buildup duration of stage (how long until state can deal damage)
26    pub buildup_duration: T,
27    /// Duration of stage spent in swing (controls animation stuff, and can also
28    /// be used to handle movement separately to buildup)
29    pub swing_duration: T,
30    /// At what fraction of the swing duration to apply the melee "hit"
31    pub hit_timing: f32,
32    /// Initial recover duration of stage (how long until character exits state)
33    pub recover_duration: T,
34    /// How much forward movement there is in the swing portion of the stage
35    #[serde(default)]
36    pub movement: StrikeMovement,
37    /// Adjusts turning rate during the attack
38    pub ori_modifier: f32,
39    #[serde(default)]
40    pub custom_combo: CustomCombo,
41}
42
43impl Strike<f32> {
44    pub fn to_duration(self) -> Strike<Duration> {
45        Strike::<Duration> {
46            melee_constructor: self.melee_constructor,
47            buildup_duration: Duration::from_secs_f32(self.buildup_duration),
48            swing_duration: Duration::from_secs_f32(self.swing_duration),
49            hit_timing: self.hit_timing,
50            recover_duration: Duration::from_secs_f32(self.recover_duration),
51            movement: self.movement,
52            ori_modifier: self.ori_modifier,
53            custom_combo: self.custom_combo,
54        }
55    }
56
57    #[must_use]
58    pub fn adjusted_by_stats(self, stats: Stats) -> Self {
59        Self {
60            melee_constructor: self.melee_constructor.adjusted_by_stats(stats),
61            buildup_duration: self.buildup_duration / stats.speed,
62            swing_duration: self.swing_duration / stats.speed,
63            hit_timing: self.hit_timing,
64            recover_duration: self.recover_duration / stats.speed,
65            movement: self.movement,
66            ori_modifier: self.ori_modifier,
67            custom_combo: self.custom_combo,
68        }
69    }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
73#[serde(deny_unknown_fields)]
74pub struct StrikeMovement {
75    pub buildup: Option<ForcedMovement>,
76    pub swing: Option<ForcedMovement>,
77    pub recover: Option<ForcedMovement>,
78}
79
80// TODO: Completely rewrite this with skill tree rework. Don't bother converting
81// to melee constructor.
82#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83/// Separated out to condense update portions of character state
84pub struct StaticData {
85    /// Data for each stage
86    pub strikes: Vec<Strike<Duration>>,
87    /// The amount of energy consumed with each swing
88    pub energy_cost_per_strike: f32,
89    /// Used to specify the attack to the frontend
90    pub specifier: Option<combo_melee2::FrontendSpecifier>,
91    /// Whether or not the state should progress through all strikes
92    /// automatically once the state is entered
93    pub auto_progress: bool,
94    pub ability_info: AbilityInfo,
95}
96/// A sequence of attacks that can incrementally become faster and more
97/// damaging.
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99pub struct Data {
100    /// Struct containing data that does not change over the course of the
101    /// character state
102    pub static_data: StaticData,
103    /// Whether the attack was executed already
104    pub exhausted: bool,
105    /// Whether the strike should skip recover
106    pub start_next_strike: bool,
107    /// Timer for each stage
108    pub timer: Duration,
109    /// Checks what section a strike is in, if a strike is currently being
110    /// performed
111    pub stage_section: StageSection,
112    /// Index of the strike that is currently in progress, or if not in a strike
113    /// currently the next strike that will occur
114    pub completed_strikes: usize,
115}
116
117impl CharacterBehavior for Data {
118    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
119        let mut update = StateUpdate::from(data);
120
121        handle_orientation(data, &mut update, 1.0, None);
122        handle_move(data, &mut update, 0.7);
123        handle_interrupts(data, &mut update, output_events);
124
125        let strike_data = self.strike_data();
126
127        match self.stage_section {
128            StageSection::Buildup => {
129                if let Some(movement) = strike_data.movement.buildup {
130                    handle_forced_movement(data, &mut update, movement);
131                }
132                if self.timer < strike_data.buildup_duration {
133                    // Build up
134                    if let CharacterState::ComboMelee2(c) = &mut update.character {
135                        c.timer = tick_attack_or_default(data, self.timer, None);
136                    }
137                } else {
138                    // Transitions to swing section of stage
139                    if let CharacterState::ComboMelee2(c) = &mut update.character {
140                        c.timer = Duration::default();
141                        c.stage_section = StageSection::Action;
142                    }
143                }
144                if let Some(FrontendSpecifier::ClayGolemDash) = self.static_data.specifier {
145                    // Send local event used for frontend shenanigans
146                    output_events.emit_local(LocalEvent::CreateOutcome(Outcome::ClayGolemDash {
147                        pos: data.pos.0,
148                    }));
149                }
150            },
151            StageSection::Action => {
152                if let Some(movement) = strike_data.movement.swing {
153                    handle_forced_movement(data, &mut update, movement);
154                }
155                if input_is_pressed(data, self.static_data.ability_info.input)
156                    || self.static_data.auto_progress
157                {
158                    if let CharacterState::ComboMelee2(c) = &mut update.character {
159                        // Only have the next strike skip the recover period of this strike if not
160                        // every strike in the combo is complete yet
161                        c.start_next_strike =
162                            (c.completed_strikes + 1) < c.static_data.strikes.len();
163                    }
164                }
165                if self.timer.as_secs_f32()
166                    > strike_data.hit_timing * strike_data.swing_duration.as_secs_f32()
167                    && !self.exhausted
168                {
169                    if let CharacterState::ComboMelee2(c) = &mut update.character {
170                        c.timer = tick_attack_or_default(data, self.timer, None);
171                        c.exhausted = true;
172                    }
173
174                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
175                    let tool_stats = get_tool_stats(data, self.static_data.ability_info);
176
177                    data.updater.insert(
178                        data.entity,
179                        strike_data
180                            .melee_constructor
181                            .custom_combo(strike_data.custom_combo)
182                            .create_melee(precision_mult, tool_stats),
183                    );
184                } else if self.timer < strike_data.swing_duration {
185                    // Swings
186                    if let CharacterState::ComboMelee2(c) = &mut update.character {
187                        c.timer = tick_attack_or_default(data, self.timer, None);
188                    }
189                    if self.static_data.specifier == Some(FrontendSpecifier::IronGolemFist) {
190                        let damage = AttackDamage::new(
191                            Damage {
192                                source: DamageSource::Explosion,
193                                kind: Crushing,
194                                value: 10.0,
195                            },
196                            Some(GroupTarget::OutOfGroup),
197                            rand::random(),
198                        );
199                        let attack = Attack::default().with_damage(damage);
200                        let explosion = Explosion {
201                            effects: vec![RadiusEffect::Attack(attack)],
202                            radius: data.body.max_radius() * 10.0,
203                            reagent: Some(Reagent::Yellow),
204                            min_falloff: 0.5,
205                        };
206                        let pos =
207                            data.pos.0 + (*data.ori.look_dir() * (data.body.max_radius() * 3.0));
208                        let explosition =
209                            Vec3::new(pos.x, pos.y, pos.z + (data.body.height() / 2.0));
210                        output_events.emit_server(ExplosionEvent {
211                            pos: explosition,
212                            explosion,
213                            owner: Some(*data.uid),
214                        });
215                    }
216                } else if self.start_next_strike {
217                    if let CharacterState::ComboMelee2(c) = &mut update.character {
218                        c.completed_strikes += 1;
219                    }
220                    next_strike(data, &mut update);
221                } else {
222                    // Transitions to recover section of stage
223                    if let CharacterState::ComboMelee2(c) = &mut update.character {
224                        c.timer = Duration::default();
225                        c.stage_section = StageSection::Recover;
226                    }
227                }
228            },
229            StageSection::Recover => {
230                if let Some(movement) = strike_data.movement.recover {
231                    handle_forced_movement(data, &mut update, movement);
232                }
233                if self.timer < strike_data.recover_duration {
234                    // Recovery
235                    if let CharacterState::ComboMelee2(c) = &mut update.character {
236                        c.timer = tick_attack_or_default(
237                            data,
238                            self.timer,
239                            Some(data.stats.recovery_speed_modifier),
240                        );
241                    }
242                } else {
243                    // Return to wielding
244                    end_melee_ability(data, &mut update);
245                }
246            },
247            _ => {
248                // If it somehow ends up in an incorrect stage section
249                end_melee_ability(data, &mut update);
250            },
251        }
252
253        update
254    }
255}
256
257impl Data {
258    pub fn strike_data(&self) -> &Strike<Duration> {
259        &self.static_data.strikes[self.completed_strikes % self.static_data.strikes.len()]
260    }
261}
262
263fn next_strike(data: &JoinData, update: &mut StateUpdate) {
264    let revert_to_wield = if let CharacterState::ComboMelee2(c) = &mut update.character {
265        if update
266            .energy
267            .try_change_by(-c.static_data.energy_cost_per_strike)
268            .is_ok()
269        {
270            c.exhausted = false;
271            c.start_next_strike = false;
272            c.timer = Duration::default();
273            c.stage_section = StageSection::Buildup;
274            false
275        } else {
276            true
277        }
278    } else {
279        false
280    };
281    if revert_to_wield {
282        end_melee_ability(data, update)
283    }
284}
285
286#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
287pub enum FrontendSpecifier {
288    ClayGolemDash,
289    IronGolemFist,
290}