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 Forward(f32),
25 Upward(f32),
27}
28
29#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct Data {
31 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 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 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 !matches!(update.character, CharacterState::Glide { .. }) {
102 return update;
103 }
104
105 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 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 Quaternion::rotation_3d(PI, *tgt_dir_ori.right()) * d
150 } else {
151 d
152 };
153 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 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 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 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}