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(Copy, 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                    update.character = CharacterState::Throw(Data {
100                        timer: tick_attack_or_default(data, self.timer, None),
101                        ..*self
102                    });
103                } else {
104                    // Transitions to swing section of stage
105                    update.character = CharacterState::Throw(Data {
106                        timer: Duration::default(),
107                        stage_section: StageSection::Charge,
108                        ..*self
109                    });
110                }
111            },
112            StageSection::Charge => {
113                if !input_is_pressed(data, self.static_data.ability_info.input)
114                    && !self.exhausted
115                    && data
116                        .inventory
117                        .and_then(|inv| inv.equipped(self.static_data.equip_slot))
118                        .is_some_and(|item| item.item_hash() == self.static_data.item_hash)
119                {
120                    let charge_frac = self.charge_frac();
121
122                    let body_offsets = data
123                        .body
124                        .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
125                    let pos = Pos(data.pos.0 + body_offsets);
126
127                    let dir = match self.static_data.projectile_dir {
128                        ProjectileDir::LookDir => data.inputs.look_dir,
129                        ProjectileDir::Upwards(interpolation_factor) => {
130                            Dir::up().slerped_to(data.inputs.look_dir, interpolation_factor)
131                        },
132                    };
133
134                    let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
135                    let projectile = if self.static_data.projectile.scaled.is_some() {
136                        self.static_data.projectile.handle_scaling(charge_frac)
137                    } else {
138                        self.static_data.projectile
139                    };
140                    let projectile = projectile.create_projectile(
141                        Some(*data.uid),
142                        precision_mult,
143                        self.static_data.damage_effect,
144                    );
145
146                    // Removes the thrown item from the entity's inventory and creates a
147                    // projectile
148                    output_events.emit_server(ThrowEvent {
149                        entity: data.entity,
150                        pos,
151                        dir,
152                        projectile,
153                        light: self.static_data.projectile_light,
154                        speed: self.static_data.initial_projectile_speed
155                            + charge_frac * self.static_data.scaled_projectile_speed,
156                        object: None,
157                        equip_slot: self.static_data.equip_slot,
158                    });
159
160                    // Exhausts ability and transitions to action
161                    update.character = CharacterState::Throw(Data {
162                        timer: Duration::default(),
163                        stage_section: StageSection::Action,
164                        exhausted: true,
165                        ..*self
166                    });
167                } else if self.timer < self.static_data.charge_duration
168                    && input_is_pressed(data, self.static_data.ability_info.input)
169                {
170                    // Charges
171                    update.character = CharacterState::Throw(Data {
172                        timer: tick_attack_or_default(data, self.timer, None),
173                        ..*self
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                    update.character = CharacterState::Throw(Data {
183                        timer: tick_attack_or_default(data, self.timer, None),
184                        ..*self
185                    });
186
187                    // Consumes energy if there's enough left and RMB is held down
188                    update
189                        .energy
190                        .change_by(-self.static_data.energy_drain * data.dt.0 / 5.0);
191                }
192            },
193            StageSection::Action => {
194                if self.timer < self.static_data.throw_duration {
195                    // Throws
196                    update.character = CharacterState::Throw(Data {
197                        timer: tick_attack_or_default(data, self.timer, None),
198                        ..*self
199                    });
200                } else {
201                    // Transition to recover
202                    update.character = CharacterState::Throw(Data {
203                        timer: Duration::default(),
204                        stage_section: StageSection::Recover,
205                        ..*self
206                    });
207                }
208            },
209            StageSection::Recover => {
210                if self.timer < self.static_data.recover_duration {
211                    // Recovers
212                    update.character = CharacterState::Throw(Data {
213                        timer: tick_attack_or_default(
214                            data,
215                            self.timer,
216                            Some(data.stats.recovery_speed_modifier),
217                        ),
218                        ..*self
219                    });
220                } else {
221                    // Done
222                    end_ability(data, &mut update);
223                }
224            },
225            _ => {
226                // If it somehow ends up in an incorrect stage section
227                end_ability(data, &mut update);
228            },
229        }
230
231        // At end of state logic so an interrupt isn't overwritten
232        handle_interrupts(data, &mut update, output_events);
233
234        update
235    }
236}