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