veloren_common/states/
glide.rs

1use super::utils::*;
2use crate::{
3    comp::{
4        CharacterState, Ori, StateUpdate, Vel, character_state::OutputEvents,
5        fluid_dynamics::angle_of_attack, inventory::slot::EquipSlot,
6    },
7    event::LocalEvent,
8    outcome::Outcome,
9    states::{
10        behavior::{CharacterBehavior, JoinData},
11        glide_wield, idle,
12    },
13    util::{Dir, Plane, Projection},
14};
15use serde::{Deserialize, Serialize};
16use std::{f32::consts::PI, time::Duration};
17use vek::*;
18
19const PITCH_SLOW_TIME: f32 = 0.5;
20
21#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
22pub enum Boost {
23    /// Slowly increases XY speed
24    Forward(f32),
25    /// Gives Z impulse
26    Upward(f32),
27}
28
29#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct Data {
31    /// The aspect ratio is the ratio of the span squared to actual planform
32    /// area
33    pub aspect_ratio: f32,
34    pub planform_area: f32,
35    pub ori: Ori,
36    last_vel: Vel,
37    pub timer: Duration,
38    inputs_disabled: bool,
39    pub booster: Option<Boost>,
40}
41
42impl Data {
43    /// A glider is modelled as an elliptical wing and has a span length
44    /// (distance from wing tip to wing tip) and a chord length (distance from
45    /// leading edge to trailing edge through its centre) measured in block
46    /// units.
47    ///
48    ///  https://en.wikipedia.org/wiki/Elliptical_wing
49    pub fn new(span_length: f32, chord_length: f32, ori: Ori) -> Self {
50        let planform_area = PI * chord_length * span_length * 0.25;
51        let aspect_ratio = span_length.powi(2) / planform_area;
52        Self {
53            aspect_ratio,
54            planform_area,
55            ori,
56            last_vel: Vel::zero(),
57            timer: Duration::default(),
58            inputs_disabled: true,
59            booster: None,
60        }
61    }
62
63    fn tgt_dir(&self, data: &JoinData) -> Dir {
64        let move_dir = if self.inputs_disabled {
65            Vec2::zero()
66        } else {
67            data.inputs.move_dir
68        };
69        let look_ori = Ori::from(data.inputs.look_dir);
70        look_ori
71            .yawed_right(PI / 3.0 * look_ori.right().xy().dot(move_dir))
72            .pitched_up(PI * 0.04)
73            .pitched_down(
74                data.inputs
75                    .look_dir
76                    .xy()
77                    .try_normalized()
78                    .map_or(0.0, |ld| {
79                        PI * 0.1 * ld.dot(move_dir) * self.timer.as_secs_f32().min(PITCH_SLOW_TIME)
80                            / PITCH_SLOW_TIME
81                    }),
82            )
83            .look_dir()
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        // reset booster
91        update.character = CharacterState::Glide(Self {
92            booster: None,
93            ..*self
94        });
95
96        let gained_booster = self.booster;
97
98        handle_glider_input_or(data, &mut update, output_events, |_, _| {});
99
100        // If switched state, let it do its thing
101        if !matches!(update.character, CharacterState::Glide { .. }) {
102            return update;
103        }
104
105        // If player is on the ground and effectively doesn't have any gliding
106        // power left, end the glide
107        if data.physics.on_ground.is_some()
108            && (data.vel.0 - data.physics.ground_vel).magnitude_squared() < 2_f32.powi(2)
109            && gained_booster.is_none()
110        {
111            update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
112        } else if data.physics.in_liquid().is_some()
113            || data
114                .inventory
115                .and_then(|inv| inv.equipped(EquipSlot::Glider))
116                .is_none()
117        {
118            update.character = CharacterState::Idle(idle::Data::default());
119        } else if !handle_climb(data, &mut update) {
120            let air_flow = data
121                .physics
122                .in_fluid
123                .map(|fluid| fluid.relative_flow(data.vel))
124                .unwrap_or_default();
125
126            let inputs_disabled = self.inputs_disabled && !data.inputs.move_dir.is_approx_zero();
127
128            let ori = {
129                let slerp_s = {
130                    let angle = self.ori.look_dir().angle_between(*data.inputs.look_dir);
131                    let rate = 0.4 * PI / angle;
132                    (data.dt.0 * rate).min(1.0)
133                };
134
135                Dir::from_unnormalized(air_flow.0)
136                    .map(|flow_dir| {
137                        let tgt_dir = self.tgt_dir(data);
138                        let tgt_dir_ori = Ori::from(tgt_dir);
139                        let tgt_dir_up = tgt_dir_ori.up();
140                        // The desired up vector of our glider.
141                        // We begin by projecting the flow dir on the plane with the normal of
142                        // our tgt_dir to get an idea of how it will hit the glider
143                        let tgt_up = flow_dir
144                            .projected(&Plane::from(tgt_dir))
145                            .map(|d| {
146                                let d = if d.dot(*tgt_dir_up).is_sign_negative() {
147                                    // when the final direction of flow is downward we don't roll
148                                    // upside down but instead mirror the target up vector
149                                    Quaternion::rotation_3d(PI, *tgt_dir_ori.right()) * d
150                                } else {
151                                    d
152                                };
153                                // slerp from untilted up towards the direction by a factor of
154                                // lateral wind to prevent overly reactive adjustments
155                                let lateral_wind_speed =
156                                    air_flow.0.projected(&self.ori.right()).magnitude();
157                                tgt_dir_up.slerped_to(d, lateral_wind_speed / 15.0)
158                            })
159                            .unwrap_or_else(Dir::up);
160                        let global_roll = tgt_dir_up.rotation_between(tgt_up);
161                        let global_pitch = angle_of_attack(&tgt_dir_ori, &flow_dir)
162                            * self.timer.as_secs_f32().min(PITCH_SLOW_TIME)
163                            / PITCH_SLOW_TIME;
164
165                        self.ori.slerped_towards(
166                            tgt_dir_ori.prerotated(global_roll).pitched_up(global_pitch),
167                            slerp_s,
168                        )
169                    })
170                    .unwrap_or_else(|| self.ori.slerped_towards(self.ori.uprighted(), slerp_s))
171            };
172
173            update.ori = {
174                let slerp_s = {
175                    let angle = data.ori.look_dir().angle_between(*data.inputs.look_dir);
176                    let rate = 0.2 * data.body.base_ori_rate() * PI / angle;
177                    (data.dt.0 * rate).min(1.0)
178                };
179
180                let rot_from_drag = {
181                    let speed_factor =
182                        air_flow.0.magnitude_squared().min(40_f32.powi(2)) / 40_f32.powi(2);
183
184                    Quaternion::rotation_3d(
185                        -PI / 2.0 * speed_factor,
186                        ori.up()
187                            .cross(air_flow.0)
188                            .try_normalized()
189                            .unwrap_or_else(|| *data.ori.right()),
190                    )
191                };
192
193                let rot_from_accel = {
194                    let accel = data.vel.0 - self.last_vel.0;
195                    let accel_factor = accel.magnitude_squared().min(1.0) / 1.0;
196
197                    Quaternion::rotation_3d(
198                        PI / 2.0
199                            * accel_factor
200                            * if data.physics.on_ground.is_some() {
201                                -1.0
202                            } else {
203                                1.0
204                            },
205                        ori.up()
206                            .cross(accel)
207                            .try_normalized()
208                            .unwrap_or_else(|| *data.ori.right()),
209                    )
210                };
211
212                update.ori.slerped_towards(
213                    ori.to_horizontal()
214                        .prerotated(rot_from_drag * rot_from_accel),
215                    slerp_s,
216                )
217            };
218
219            // If we gained a booster
220            if let Some(booster) = gained_booster {
221                match booster {
222                    Boost::Upward(speed) => {
223                        update.vel.0.z += speed * data.dt.0;
224                    },
225                    Boost::Forward(speed) => {
226                        if data.physics.on_ground.is_some() {
227                            // quality of life hack: help with starting
228                            //
229                            // other velocities are intentionally ignored
230                            update.vel.0.z += 500.0 * data.dt.0;
231                        } else {
232                            update.vel.0.x *= 1.0 + speed * data.dt.0;
233                            update.vel.0.y *= 1.0 + speed * data.dt.0;
234                        }
235                    },
236                }
237            };
238
239            // Don't override gained booster, if any, otherwise set to None
240            let next_booster = if let CharacterState::Glide(Data {
241                booster: Some(booster),
242                ..
243            }) = update.character
244            {
245                Some(booster)
246            } else {
247                None
248            };
249
250            update.character = CharacterState::Glide(Self {
251                ori,
252                last_vel: *data.vel,
253                timer: tick_attack_or_default(data, self.timer, None),
254                inputs_disabled,
255                booster: next_booster,
256                ..*self
257            });
258        }
259
260        update
261    }
262
263    fn unwield(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
264        let mut update = StateUpdate::from(data);
265        output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Glider {
266            pos: data.pos.0,
267            wielded: false,
268        }));
269        update.character = CharacterState::Idle(idle::Data::default());
270        update
271    }
272}