Skip to main content

veloren_common/states/
throw.rs

1use crate::{
2    combat::{self, CombatEffect},
3    comp::{
4        CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate,
5        character_state::OutputEvents, item::ToolKind, slot::EquipSlot,
6    },
7    event::ThrowEvent,
8    states::{
9        behavior::{CharacterBehavior, JoinData},
10        utils::*,
11    },
12    util::Dir,
13};
14use serde::{Deserialize, Serialize};
15use std::time::Duration;
16
17#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
18pub enum ProjectileDir {
19    // Projectile is thrown in the direction the entity is looking
20    LookDir,
21    // Projectile is thrown in the direction of an upwards facing vector slerped by some factor
22    // towards the look direction
23    Upwards(f32),
24}
25
26/// Separated out to condense update portions of character state
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct StaticData {
29    /// How long the weapon needs to be prepared for
30    pub buildup_duration: Duration,
31    /// How long it takes to charge the weapon to max damage and knockback
32    pub charge_duration: Duration,
33    /// How long the action stage lasts
34    pub throw_duration: Duration,
35    /// How long the state has until exiting
36    pub recover_duration: Duration,
37    /// How much energy is drained per second when charging
38    pub energy_drain: f32,
39    /// Projectile information
40    pub projectile: ProjectileConstructor,
41    pub projectile_light: Option<LightEmitter>,
42    pub projectile_dir: ProjectileDir,
43    pub initial_projectile_speed: f32,
44    pub scaled_projectile_speed: f32,
45    /// Move speed efficiency
46    pub move_speed: f32,
47    /// What key is used to press ability
48    pub ability_info: AbilityInfo,
49    /// Adds an effect onto the main damage of the attack
50    pub damage_effect: Option<CombatEffect>,
51    /// Inventory slot to use item from
52    pub equip_slot: EquipSlot,
53    /// Item hash, used to verify that slot still has the correct item
54    pub item_hash: u64,
55    /// Type of tool thrown, stored here since it cannot be recalculated once
56    /// the thrown item is removed from the entity's inventory
57    pub tool_kind: ToolKind,
58    /// Hand info for the thrown tool, stored here since it cannot be
59    /// recalculated once the thrown item is removed from the entity's inventory
60    pub hand_info: HandInfo,
61}
62
63#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64pub struct Data {
65    /// Struct containing data that does not change over the course of the
66    /// character state
67    pub static_data: StaticData,
68    /// Timer for each stage
69    pub timer: Duration,
70    /// What section the character stage is in
71    pub stage_section: StageSection,
72    /// Whether the attack fired already
73    pub exhausted: bool,
74}
75
76impl Data {
77    /// How complete the charge is, on a scale of 0.0 to 1.0
78    pub fn charge_frac(&self) -> f32 {
79        if let StageSection::Charge = self.stage_section {
80            (self.timer.as_secs_f32() / self.static_data.charge_duration.as_secs_f32()).min(1.0)
81        } else {
82            0.0
83        }
84    }
85}
86
87impl CharacterBehavior for Data {
88    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
89        let mut update = StateUpdate::from(data);
90
91        handle_orientation(data, &mut update, 1.0, None);
92        handle_move(data, &mut update, self.static_data.move_speed);
93        handle_jump(data, output_events, &mut update, 1.0);
94
95        match self.stage_section {
96            StageSection::Buildup => {
97                if self.timer < self.static_data.buildup_duration {
98                    // Build up
99                    if let CharacterState::Throw(c) = &mut update.character {
100                        c.timer = tick_attack_or_default(data, self.timer, None);
101                    }
102                } else {
103                    // Transitions to swing section of stage
104                    if let CharacterState::Throw(c) = &mut update.character {
105                        c.timer = Duration::default();
106                        c.stage_section = StageSection::Charge;
107                    }
108                }
109            },
110            StageSection::Charge => {
111                if !input_is_pressed(data, self.static_data.ability_info.input)
112                    && !self.exhausted
113                    && data
114                        .inventory
115                        .and_then(|inv| inv.equipped(self.static_data.equip_slot))
116                        .is_some_and(|item| item.item_hash() == self.static_data.item_hash)
117                {
118                    let charge_frac = self.charge_frac();
119
120                    let body_offsets = data
121                        .body
122                        .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
123                    let pos = Pos(data.pos.0 + body_offsets);
124
125                    let dir = match self.static_data.projectile_dir {
126                        ProjectileDir::LookDir => data.inputs.look_dir,
127                        ProjectileDir::Upwards(interpolation_factor) => {
128                            Dir::up().slerped_to(data.inputs.look_dir, interpolation_factor)
129                        },
130                    };
131
132                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
133                    let projectile = {
134                        let projectile = self.static_data.projectile.clone();
135                        if self.static_data.projectile.scaled.is_some() {
136                            projectile.handle_scaling(charge_frac)
137                        } else {
138                            projectile
139                        }
140                    };
141                    let (projectile, _marker) = projectile.create_projectile(
142                        Some(*data.uid),
143                        precision_mult,
144                        Some(self.static_data.ability_info),
145                        Some(data.stats),
146                    );
147
148                    // Removes the thrown item from the entity's inventory and creates a
149                    // projectile
150                    output_events.emit_server(ThrowEvent {
151                        entity: data.entity,
152                        pos,
153                        dir,
154                        projectile,
155                        light: self.static_data.projectile_light,
156                        speed: self.static_data.initial_projectile_speed
157                            + charge_frac * self.static_data.scaled_projectile_speed,
158                        object: None,
159                        equip_slot: self.static_data.equip_slot,
160                    });
161
162                    // Exhausts ability and transitions to action
163                    if let CharacterState::Throw(c) = &mut update.character {
164                        c.timer = Duration::default();
165                        c.stage_section = StageSection::Action;
166                        c.exhausted = true;
167                    }
168                } else if self.timer < self.static_data.charge_duration
169                    && input_is_pressed(data, self.static_data.ability_info.input)
170                {
171                    // Charges
172                    if let CharacterState::Throw(c) = &mut update.character {
173                        c.timer = tick_attack_or_default(data, self.timer, None);
174                    }
175
176                    // Consumes energy if there's enough left and input is held down
177                    update
178                        .energy
179                        .change_by(-self.static_data.energy_drain * data.dt.0);
180                } else if input_is_pressed(data, self.static_data.ability_info.input) {
181                    // Holds charge
182                    if let CharacterState::Throw(c) = &mut update.character {
183                        c.timer = tick_attack_or_default(data, self.timer, None);
184                    }
185
186                    // Consumes energy if there's enough left and RMB is held down
187                    update
188                        .energy
189                        .change_by(-self.static_data.energy_drain * data.dt.0 / 5.0);
190                }
191            },
192            StageSection::Action => {
193                if self.timer < self.static_data.throw_duration {
194                    // Throws
195                    if let CharacterState::Throw(c) = &mut update.character {
196                        c.timer = tick_attack_or_default(data, self.timer, None);
197                    }
198                } else {
199                    // Transition to recover
200                    if let CharacterState::Throw(c) = &mut update.character {
201                        c.timer = Duration::default();
202                        c.stage_section = StageSection::Recover;
203                    }
204                }
205            },
206            StageSection::Recover => {
207                if self.timer < self.static_data.recover_duration {
208                    // Recovers
209                    if let CharacterState::Throw(c) = &mut update.character {
210                        c.timer = tick_attack_or_default(data, self.timer, None);
211                    }
212                } else {
213                    // Done
214                    end_ability(data, &mut update);
215                }
216            },
217            _ => {
218                // If it somehow ends up in an incorrect stage section
219                end_ability(data, &mut update);
220            },
221        }
222
223        // At end of state logic so an interrupt isn't overwritten
224        handle_interrupts(data, &mut update, output_events);
225
226        update
227    }
228}