1use crate::math::{AdjustPrecision, Float, Vector3};
2use bevy::prelude::*;
3use bevy::time::Stopwatch;
4
5use crate::util::{
6 calc_angular_velchange_to_force_forward, SegmentedJumpDurationCalculator,
7 SegmentedJumpInitialVelocityCalculator, VelocityBoundary,
8};
9use crate::{
10 TnuaAction, TnuaActionContext, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
11 TnuaActionLifecycleStatus,
12};
13
14#[derive(Clone, Debug)]
25pub struct TnuaBuiltinJump {
26 pub height: Float,
37
38 pub vertical_displacement: Option<Vector3>,
39
40 pub allow_in_air: bool,
42
43 pub upslope_extra_gravity: Float,
50
51 pub takeoff_extra_gravity: Float,
58
59 pub takeoff_above_velocity: Float,
64
65 pub fall_extra_gravity: Float,
69
70 pub shorten_extra_gravity: Float,
74
75 pub peak_prevention_at_upward_velocity: Float,
84
85 pub peak_prevention_extra_gravity: Float,
89
90 pub reschedule_cooldown: Option<Float>,
103
104 pub input_buffer_time: Float,
108
109 pub force_forward: Option<Dir3>,
116
117 pub disable_force_forward_after_peak: bool,
118}
119
120impl Default for TnuaBuiltinJump {
121 fn default() -> Self {
122 Self {
123 height: 0.0,
124 vertical_displacement: None,
125 allow_in_air: false,
126 upslope_extra_gravity: 30.0,
127 takeoff_extra_gravity: 30.0,
128 takeoff_above_velocity: 2.0,
129 fall_extra_gravity: 20.0,
130 shorten_extra_gravity: 60.0,
131 peak_prevention_at_upward_velocity: 1.0,
132 peak_prevention_extra_gravity: 20.0,
133 reschedule_cooldown: None,
134 input_buffer_time: 0.2,
135 force_forward: None,
136 disable_force_forward_after_peak: true,
137 }
138 }
139}
140
141impl TnuaAction for TnuaBuiltinJump {
142 const NAME: &'static str = "TnuaBuiltinJump";
143 type State = TnuaBuiltinJumpState;
144 const VIOLATES_COYOTE_TIME: bool = true;
145
146 fn initiation_decision(
147 &self,
148 ctx: TnuaActionContext,
149 being_fed_for: &Stopwatch,
150 ) -> crate::basis_action_traits::TnuaActionInitiationDirective {
151 if self.allow_in_air || !ctx.basis.is_airborne() {
152 TnuaActionInitiationDirective::Allow
154 } else if (being_fed_for.elapsed().as_secs_f64() as Float) < self.input_buffer_time {
155 TnuaActionInitiationDirective::Delay
156 } else {
157 TnuaActionInitiationDirective::Reject
158 }
159 }
160
161 fn apply(
162 &self,
163 state: &mut Self::State,
164 ctx: TnuaActionContext,
165 lifecycle_status: TnuaActionLifecycleStatus,
166 motor: &mut crate::TnuaMotor,
167 ) -> TnuaActionLifecycleDirective {
168 let up = ctx.up_direction.adjust_precision();
169
170 if lifecycle_status.just_started() {
171 let mut calculator = SegmentedJumpInitialVelocityCalculator::new(self.height);
172 let gravity = ctx.tracker.gravity.dot(-up);
173 let kinetic_energy = calculator
174 .add_segment(
175 gravity + self.peak_prevention_extra_gravity,
176 self.peak_prevention_at_upward_velocity,
177 )
178 .add_segment(gravity, self.takeoff_above_velocity)
179 .add_final_segment(gravity + self.takeoff_extra_gravity)
180 .kinetic_energy()
181 .expect("`add_final_segment` should have covered remaining height");
182 *state = TnuaBuiltinJumpState::StartingJump {
183 origin: ctx.tracker.translation,
184 desired_energy: kinetic_energy,
185 };
186 }
187
188 let effective_velocity = ctx.basis.effective_velocity();
189
190 if let Some(force_forward) = self.force_forward {
191 let disable_force_forward = self.disable_force_forward_after_peak
192 && match state {
193 TnuaBuiltinJumpState::NoJump => true,
194 TnuaBuiltinJumpState::StartingJump { .. } => false,
195 TnuaBuiltinJumpState::SlowDownTooFastSlopeJump { .. } => false,
196 TnuaBuiltinJumpState::MaintainingJump { .. } => false,
197 TnuaBuiltinJumpState::StoppedMaintainingJump => true,
198 TnuaBuiltinJumpState::FallSection => true,
199 };
200 if !disable_force_forward {
201 motor
202 .ang
203 .cancel_on_axis(ctx.up_direction.adjust_precision());
204 motor.ang += calc_angular_velchange_to_force_forward(
205 force_forward,
206 ctx.tracker.rotation,
207 ctx.tracker.angvel,
208 ctx.up_direction,
209 ctx.frame_duration,
210 );
211 }
212 }
213
214 for _ in 0..7 {
217 return match state {
218 TnuaBuiltinJumpState::NoJump => panic!(),
219 TnuaBuiltinJumpState::StartingJump {
220 origin,
221 desired_energy,
222 } => {
223 let extra_height = if let Some(displacement) = ctx.basis.displacement() {
224 displacement.dot(up)
225 } else if !self.allow_in_air && ctx.basis.is_airborne() {
226 return self.directive_simple_or_reschedule(lifecycle_status);
227 } else {
228 0.0
230 };
231 let gravity = ctx.tracker.gravity.dot(-up);
232 let energy_from_extra_height = extra_height * gravity;
233 let desired_kinetic_energy = *desired_energy - energy_from_extra_height;
234 let desired_upward_velocity =
235 SegmentedJumpInitialVelocityCalculator::kinetic_energy_to_velocity(
236 desired_kinetic_energy,
237 );
238
239 let relative_velocity =
240 effective_velocity.dot(up) - ctx.basis.vertical_velocity().max(0.0);
241
242 motor.lin.cancel_on_axis(up);
243 motor.lin.boost += (desired_upward_velocity - relative_velocity) * up;
244 if 0.0 <= extra_height {
245 *state = TnuaBuiltinJumpState::SlowDownTooFastSlopeJump {
246 origin: *origin,
247 desired_energy: *desired_energy,
248 zero_potential_energy_at: ctx.tracker.translation - extra_height * up,
249 };
250 }
251 self.directive_simple_or_reschedule(lifecycle_status)
252 }
253 TnuaBuiltinJumpState::SlowDownTooFastSlopeJump {
254 origin,
255 desired_energy,
256 zero_potential_energy_at,
257 } => {
258 let upward_velocity = up.dot(effective_velocity);
259 if upward_velocity <= ctx.basis.vertical_velocity() {
260 *state = TnuaBuiltinJumpState::FallSection;
261 continue;
262 } else if !lifecycle_status.is_active() {
263 *state = TnuaBuiltinJumpState::StoppedMaintainingJump;
264 continue;
265 }
266 let relative_velocity = effective_velocity.dot(up);
267 let extra_height =
268 (ctx.tracker.translation - *zero_potential_energy_at).dot(up);
269 let gravity = ctx.tracker.gravity.dot(-up);
270 let energy_from_extra_height = extra_height * gravity;
271 let desired_kinetic_energy = *desired_energy - energy_from_extra_height;
272 let desired_upward_velocity =
273 SegmentedJumpInitialVelocityCalculator::kinetic_energy_to_velocity(
274 desired_kinetic_energy,
275 );
276 if relative_velocity <= desired_upward_velocity {
277 let mut velocity_boundary = None;
278 if let Some(vertical_displacement) = self.vertical_displacement {
279 let vertical_displacement = vertical_displacement
280 .reject_from(ctx.up_direction.adjust_precision());
281 let already_moved = (ctx.tracker.translation - *origin)
282 .project_onto(vertical_displacement.normalize_or_zero());
283 let duration_to_top =
284 SegmentedJumpDurationCalculator::new(relative_velocity)
285 .add_segment(
286 gravity + self.takeoff_extra_gravity,
287 self.takeoff_above_velocity,
288 )
289 .add_segment(gravity, self.peak_prevention_at_upward_velocity)
290 .add_segment(gravity + self.peak_prevention_extra_gravity, 0.0)
291 .duration();
292 let desired_vertical_velocity =
293 (vertical_displacement - already_moved) / duration_to_top;
294 let desired_boost = (desired_vertical_velocity - effective_velocity)
295 .reject_from(ctx.up_direction.adjust_precision());
296 motor.lin.boost += desired_boost;
297 velocity_boundary = VelocityBoundary::new(
298 effective_velocity.reject_from(ctx.up_direction.adjust_precision()),
299 desired_vertical_velocity,
300 0.0,
301 );
302 }
303 *state = TnuaBuiltinJumpState::MaintainingJump {
304 wait_one_frame_before_updating_velocity_boundary: true,
305 velocity_boundary,
306 };
307 continue;
308 } else {
309 let mut extra_gravity = self.upslope_extra_gravity;
310 if self.takeoff_above_velocity <= relative_velocity {
311 extra_gravity += self.takeoff_extra_gravity;
312 }
313 motor.lin.cancel_on_axis(up);
314 motor.lin.acceleration = -extra_gravity * up;
315 self.directive_simple_or_reschedule(lifecycle_status)
316 }
317 }
318 TnuaBuiltinJumpState::MaintainingJump {
319 wait_one_frame_before_updating_velocity_boundary,
320 velocity_boundary,
321 } => {
322 if let Some(velocity_boundary) = velocity_boundary {
323 if *wait_one_frame_before_updating_velocity_boundary {
324 *wait_one_frame_before_updating_velocity_boundary = false;
325 } else {
326 velocity_boundary.update(
327 ctx.basis.effective_velocity(),
328 ctx.frame_duration_as_duration(),
329 );
330 }
331 if let Some((component_direction, component_limit)) = velocity_boundary
332 .calc_boost_part_on_boundary_axis_after_limit(
333 ctx.basis.effective_velocity(),
334 motor.lin.calc_boost(ctx.frame_duration),
335 0.0,
337 1.0,
338 )
339 {
340 motor.lin.apply_boost_limit(
341 ctx.frame_duration,
342 component_direction,
343 component_limit,
344 );
345 }
346 }
347
348 let relevant_upward_velocity = effective_velocity.dot(up);
349 if relevant_upward_velocity <= 0.0 {
350 *state = TnuaBuiltinJumpState::FallSection;
351 motor.lin.cancel_on_axis(up);
352 } else {
353 motor.lin.cancel_on_axis(up);
354 if relevant_upward_velocity < self.peak_prevention_at_upward_velocity {
355 motor.lin.acceleration -= self.peak_prevention_extra_gravity * up;
356 } else if self.takeoff_above_velocity <= relevant_upward_velocity {
357 motor.lin.acceleration -= self.takeoff_extra_gravity * up;
358 }
359 }
360 match lifecycle_status {
361 TnuaActionLifecycleStatus::Initiated
362 | TnuaActionLifecycleStatus::CancelledFrom
363 | TnuaActionLifecycleStatus::StillFed => {
364 TnuaActionLifecycleDirective::StillActive
365 }
366 TnuaActionLifecycleStatus::CancelledInto => self.finish_or_reschedule(),
367 TnuaActionLifecycleStatus::NoLongerFed => {
368 *state = TnuaBuiltinJumpState::StoppedMaintainingJump;
369 TnuaActionLifecycleDirective::StillActive
370 }
371 }
372 }
373 TnuaBuiltinJumpState::StoppedMaintainingJump => {
374 if matches!(lifecycle_status, TnuaActionLifecycleStatus::CancelledInto) {
375 self.finish_or_reschedule()
376 } else {
377 let landed = ctx
378 .basis
379 .displacement()
380 .is_some_and(|displacement| displacement.dot(up) <= 0.0);
381 if landed {
382 self.finish_or_reschedule()
383 } else {
384 let upward_velocity = up.dot(effective_velocity);
385 if upward_velocity <= 0.0 {
386 *state = TnuaBuiltinJumpState::FallSection;
387 continue;
388 }
389
390 let extra_gravity = if self.takeoff_above_velocity <= upward_velocity {
391 self.shorten_extra_gravity + self.takeoff_extra_gravity
392 } else {
393 self.shorten_extra_gravity
394 };
395
396 motor.lin.cancel_on_axis(up);
397 motor.lin.acceleration -= extra_gravity * up;
398 TnuaActionLifecycleDirective::StillActive
399 }
400 }
401 }
402 TnuaBuiltinJumpState::FallSection => {
403 let landed = ctx
404 .basis
405 .displacement()
406 .is_some_and(|displacement| displacement.dot(up) <= 0.0);
407 if landed
408 || matches!(lifecycle_status, TnuaActionLifecycleStatus::CancelledInto)
409 {
410 self.finish_or_reschedule()
411 } else {
412 motor.lin.cancel_on_axis(up);
413 motor.lin.acceleration -= self.fall_extra_gravity * up;
414 TnuaActionLifecycleDirective::StillActive
415 }
416 }
417 };
418 }
419 error!("Tnua could not decide on jump state");
420 TnuaActionLifecycleDirective::Finished
421 }
422}
423
424impl TnuaBuiltinJump {
425 fn finish_or_reschedule(&self) -> TnuaActionLifecycleDirective {
426 if let Some(cooldown) = self.reschedule_cooldown {
427 TnuaActionLifecycleDirective::Reschedule {
428 after_seconds: cooldown,
429 }
430 } else {
431 TnuaActionLifecycleDirective::Finished
432 }
433 }
434
435 fn directive_simple_or_reschedule(
436 &self,
437 lifecycle_status: TnuaActionLifecycleStatus,
438 ) -> TnuaActionLifecycleDirective {
439 if let Some(cooldown) = self.reschedule_cooldown {
440 lifecycle_status.directive_simple_reschedule(cooldown)
441 } else {
442 lifecycle_status.directive_simple()
443 }
444 }
445}
446
447#[derive(Default, Debug, Clone)]
448pub enum TnuaBuiltinJumpState {
449 #[default]
450 NoJump,
451 StartingJump {
453 origin: Vector3,
454 desired_energy: Float,
461 },
462 SlowDownTooFastSlopeJump {
463 origin: Vector3,
464 desired_energy: Float,
465 zero_potential_energy_at: Vector3,
466 },
467 MaintainingJump {
468 wait_one_frame_before_updating_velocity_boundary: bool,
469 velocity_boundary: Option<VelocityBoundary>,
470 },
471 StoppedMaintainingJump,
472 FallSection,
473}