1use crate::basis_capabilities::{
2 TnuaBasisWithDisplacement, TnuaBasisWithEffectiveVelocity, TnuaBasisWithGround,
3};
4use crate::util::{
5 SegmentedJumpDurationCalculator, SegmentedJumpInitialVelocityCalculator, VelocityBoundary,
6 calc_angular_velchange_to_force_forward,
7};
8use crate::{TnuaAction, TnuaActionContext, TnuaBasis};
9use crate::{
10 TnuaActionInitiationDirective, TnuaActionLifecycleDirective, TnuaActionLifecycleStatus, math::*,
11};
12use bevy::prelude::*;
13use bevy::time::Stopwatch;
14use serde::{Deserialize, Serialize};
15
16#[derive(Default)]
28#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
29pub struct TnuaBuiltinJump {
30 pub horizontal_displacement: Option<Vector3>,
34
35 pub allow_in_air: bool,
37
38 pub force_forward: Option<Dir3>,
45}
46
47#[derive(Clone, Serialize, Deserialize)]
48pub struct TnuaBuiltinJumpConfig {
49 pub height: Float,
60
61 pub upslope_extra_gravity: Float,
68
69 pub takeoff_extra_gravity: Float,
76
77 pub takeoff_above_velocity: Float,
82
83 pub fall_extra_gravity: Float,
87
88 pub shorten_extra_gravity: Float,
92
93 pub peak_prevention_at_upward_velocity: Float,
102
103 pub peak_prevention_extra_gravity: Float,
107
108 pub reschedule_cooldown: Option<Float>,
121
122 pub input_buffer_time: Float,
126
127 pub horizontal_distance: Float,
130
131 pub disable_force_forward_after_peak: bool,
135}
136
137impl Default for TnuaBuiltinJumpConfig {
138 fn default() -> Self {
139 Self {
140 height: 0.0,
141 upslope_extra_gravity: 30.0,
142 takeoff_extra_gravity: 30.0,
143 takeoff_above_velocity: 2.0,
144 fall_extra_gravity: 20.0,
145 shorten_extra_gravity: 60.0,
146 peak_prevention_at_upward_velocity: 1.0,
147 peak_prevention_extra_gravity: 20.0,
148 reschedule_cooldown: None,
149 input_buffer_time: 0.2,
150 horizontal_distance: 1.0,
151 disable_force_forward_after_peak: true,
152 }
153 }
154}
155
156impl TnuaBuiltinJumpConfig {
157 fn finish_or_reschedule(&self) -> TnuaActionLifecycleDirective {
158 if let Some(cooldown) = self.reschedule_cooldown {
159 TnuaActionLifecycleDirective::Reschedule {
160 after_seconds: cooldown,
161 }
162 } else {
163 TnuaActionLifecycleDirective::Finished
164 }
165 }
166
167 fn directive_simple_or_reschedule(
168 &self,
169 lifecycle_status: TnuaActionLifecycleStatus,
170 ) -> TnuaActionLifecycleDirective {
171 if let Some(cooldown) = self.reschedule_cooldown {
172 lifecycle_status.directive_simple_reschedule(cooldown)
173 } else {
174 lifecycle_status.directive_simple()
175 }
176 }
177}
178
179#[derive(Default)]
180#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
181pub enum TnuaBuiltinJumpMemory {
182 #[default]
183 NoJump,
184 StartingJump {
186 origin: Vector3,
187 desired_energy: Float,
194 },
195 SlowDownTooFastSlopeJump {
196 origin: Vector3,
197 desired_energy: Float,
198 zero_potential_energy_at: Vector3,
199 },
200 MaintainingJump {
201 wait_one_frame_before_updating_velocity_boundary: bool,
202 velocity_boundary: Option<VelocityBoundary>,
203 },
204 StoppedMaintainingJump,
205 FallSection,
206}
207
208impl<B: TnuaBasis> TnuaAction<B> for TnuaBuiltinJump
209where
210 B: TnuaBasisWithEffectiveVelocity,
211 B: TnuaBasisWithDisplacement,
212 B: TnuaBasisWithGround,
213{
214 type Config = TnuaBuiltinJumpConfig;
215 type Memory = TnuaBuiltinJumpMemory;
216
217 fn initiation_decision(
218 &self,
219 config: &Self::Config,
220 _sensors: &B::Sensors<'_>,
221 ctx: TnuaActionContext<B>,
222 being_fed_for: &Stopwatch,
223 ) -> TnuaActionInitiationDirective {
224 if self.allow_in_air || !B::is_airborne(ctx.basis) {
225 TnuaActionInitiationDirective::Allow
227 } else if (being_fed_for.elapsed().as_secs_f64() as Float) < config.input_buffer_time {
228 TnuaActionInitiationDirective::Delay
229 } else {
230 TnuaActionInitiationDirective::Reject
231 }
232 }
233
234 fn apply(
235 &self,
236 config: &Self::Config,
237 memory: &mut Self::Memory,
238 _sensors: &B::Sensors<'_>,
239 ctx: TnuaActionContext<B>,
240 lifecycle_status: TnuaActionLifecycleStatus,
241 motor: &mut bevy_tnua_physics_integration_layer::data_for_backends::TnuaMotor,
242 ) -> TnuaActionLifecycleDirective {
243 let up = ctx.up_direction.adjust_precision();
244
245 if lifecycle_status.just_started() {
246 let mut calculator = SegmentedJumpInitialVelocityCalculator::new(config.height);
247 let gravity = ctx.tracker.gravity.dot(-up);
248 let kinetic_energy = calculator
249 .add_segment(
250 gravity + config.peak_prevention_extra_gravity,
251 config.peak_prevention_at_upward_velocity,
252 )
253 .add_segment(gravity, config.takeoff_above_velocity)
254 .add_final_segment(gravity + config.takeoff_extra_gravity)
255 .kinetic_energy()
256 .expect("`add_final_segment` should have covered remaining height");
257 *memory = TnuaBuiltinJumpMemory::StartingJump {
258 origin: ctx.tracker.translation,
259 desired_energy: kinetic_energy,
260 };
261 }
262
263 let effective_velocity = B::effective_velocity(ctx.basis);
264
265 if let Some(force_forward) = self.force_forward {
266 let disable_force_forward = config.disable_force_forward_after_peak
267 && match memory {
268 TnuaBuiltinJumpMemory::NoJump => true,
269 TnuaBuiltinJumpMemory::StartingJump { .. } => false,
270 TnuaBuiltinJumpMemory::SlowDownTooFastSlopeJump { .. } => false,
271 TnuaBuiltinJumpMemory::MaintainingJump { .. } => false,
272 TnuaBuiltinJumpMemory::StoppedMaintainingJump => true,
273 TnuaBuiltinJumpMemory::FallSection => true,
274 };
275 if !disable_force_forward {
276 motor
277 .ang
278 .cancel_on_axis(ctx.up_direction.adjust_precision());
279 motor.ang += calc_angular_velchange_to_force_forward(
280 force_forward,
281 ctx.tracker.rotation,
282 ctx.tracker.angvel,
283 ctx.up_direction,
284 ctx.frame_duration,
285 );
286 }
287 }
288
289 for _ in 0..7 {
292 return match memory {
293 TnuaBuiltinJumpMemory::NoJump => panic!(),
294 TnuaBuiltinJumpMemory::StartingJump {
295 origin,
296 desired_energy,
297 } => {
298 let extra_height = if let Some(displacement) = B::displacement(ctx.basis) {
299 displacement.dot(up)
300 } else if !self.allow_in_air && B::is_airborne(ctx.basis) {
301 return config.directive_simple_or_reschedule(lifecycle_status);
302 } else {
303 0.0
305 };
306 let gravity = ctx.tracker.gravity.dot(-up);
307 let energy_from_extra_height = extra_height * gravity;
308 let desired_kinetic_energy = *desired_energy - energy_from_extra_height;
309 let desired_upward_velocity =
310 SegmentedJumpInitialVelocityCalculator::kinetic_energy_to_velocity(
311 desired_kinetic_energy,
312 );
313
314 let relative_velocity =
315 effective_velocity.dot(up) - B::vertical_velocity(ctx.basis).max(0.0);
316
317 motor.lin.cancel_on_axis(up);
318 motor.lin.boost += (desired_upward_velocity - relative_velocity) * up;
319 if 0.0 <= extra_height {
320 *memory = TnuaBuiltinJumpMemory::SlowDownTooFastSlopeJump {
321 origin: *origin,
322 desired_energy: *desired_energy,
323 zero_potential_energy_at: ctx.tracker.translation - extra_height * up,
324 };
325 }
326 config.directive_simple_or_reschedule(lifecycle_status)
327 }
328 TnuaBuiltinJumpMemory::SlowDownTooFastSlopeJump {
329 origin,
330 desired_energy,
331 zero_potential_energy_at,
332 } => {
333 let upward_velocity = up.dot(effective_velocity);
334 if upward_velocity <= B::vertical_velocity(ctx.basis) {
335 *memory = TnuaBuiltinJumpMemory::FallSection;
336 continue;
337 } else if !lifecycle_status.is_active() {
338 *memory = TnuaBuiltinJumpMemory::StoppedMaintainingJump;
339 continue;
340 }
341 let relative_velocity = effective_velocity.dot(up);
342 let extra_height =
343 (ctx.tracker.translation - *zero_potential_energy_at).dot(up);
344 let gravity = ctx.tracker.gravity.dot(-up);
345 let energy_from_extra_height = extra_height * gravity;
346 let desired_kinetic_energy = *desired_energy - energy_from_extra_height;
347 let desired_upward_velocity =
348 SegmentedJumpInitialVelocityCalculator::kinetic_energy_to_velocity(
349 desired_kinetic_energy,
350 );
351 if relative_velocity <= desired_upward_velocity {
352 let mut velocity_boundary = None;
353 if let Some(horizontal_displacement) = self.horizontal_displacement {
354 let horizontal_displacement = config.horizontal_distance
355 * horizontal_displacement
356 .reject_from(ctx.up_direction.adjust_precision());
357 let already_moved = (ctx.tracker.translation - *origin)
358 .project_onto(horizontal_displacement.normalize_or_zero());
359 let duration_to_top =
360 SegmentedJumpDurationCalculator::new(relative_velocity)
361 .add_segment(
362 gravity + config.takeoff_extra_gravity,
363 config.takeoff_above_velocity,
364 )
365 .add_segment(gravity, config.peak_prevention_at_upward_velocity)
366 .add_segment(
367 gravity + config.peak_prevention_extra_gravity,
368 0.0,
369 )
370 .duration();
371 let desired_vertical_velocity =
372 (horizontal_displacement - already_moved) / duration_to_top;
373 let desired_boost = (desired_vertical_velocity - effective_velocity)
374 .reject_from(ctx.up_direction.adjust_precision());
375 motor.lin.boost += desired_boost;
376 velocity_boundary = VelocityBoundary::new(
377 effective_velocity.reject_from(ctx.up_direction.adjust_precision()),
378 desired_vertical_velocity,
379 0.0,
380 );
381 }
382 *memory = TnuaBuiltinJumpMemory::MaintainingJump {
383 wait_one_frame_before_updating_velocity_boundary: true,
384 velocity_boundary,
385 };
386 continue;
387 } else {
388 let mut extra_gravity = config.upslope_extra_gravity;
389 if config.takeoff_above_velocity <= relative_velocity {
390 extra_gravity += config.takeoff_extra_gravity;
391 }
392 motor.lin.cancel_on_axis(up);
393 motor.lin.acceleration = -extra_gravity * up;
394 config.directive_simple_or_reschedule(lifecycle_status)
395 }
396 }
397 TnuaBuiltinJumpMemory::MaintainingJump {
398 wait_one_frame_before_updating_velocity_boundary,
399 velocity_boundary,
400 } => {
401 if let Some(velocity_boundary) = velocity_boundary {
402 if *wait_one_frame_before_updating_velocity_boundary {
403 *wait_one_frame_before_updating_velocity_boundary = false;
404 } else {
405 velocity_boundary.update(
406 B::effective_velocity(ctx.basis),
407 ctx.frame_duration_as_duration(),
408 );
409 }
410 if let Some((component_direction, component_limit)) = velocity_boundary
411 .calc_boost_part_on_boundary_axis_after_limit(
412 B::effective_velocity(ctx.basis),
413 motor.lin.calc_boost(ctx.frame_duration),
414 0.0,
416 1.0,
417 )
418 {
419 motor.lin.apply_boost_limit(
420 ctx.frame_duration,
421 component_direction,
422 component_limit,
423 );
424 }
425 }
426
427 let relevant_upward_velocity = effective_velocity.dot(up);
428 if relevant_upward_velocity <= 0.0 {
429 *memory = TnuaBuiltinJumpMemory::FallSection;
430 motor.lin.cancel_on_axis(up);
431 } else {
432 motor.lin.cancel_on_axis(up);
433 if relevant_upward_velocity < config.peak_prevention_at_upward_velocity {
434 motor.lin.acceleration -= config.peak_prevention_extra_gravity * up;
435 } else if config.takeoff_above_velocity <= relevant_upward_velocity {
436 motor.lin.acceleration -= config.takeoff_extra_gravity * up;
437 }
438 }
439 match lifecycle_status {
440 TnuaActionLifecycleStatus::Initiated
441 | TnuaActionLifecycleStatus::CancelledFrom
442 | TnuaActionLifecycleStatus::StillFed => {
443 TnuaActionLifecycleDirective::StillActive
444 }
445 TnuaActionLifecycleStatus::CancelledInto => config.finish_or_reschedule(),
446 TnuaActionLifecycleStatus::NoLongerFed => {
447 *memory = TnuaBuiltinJumpMemory::StoppedMaintainingJump;
448 TnuaActionLifecycleDirective::StillActive
449 }
450 }
451 }
452 TnuaBuiltinJumpMemory::StoppedMaintainingJump => {
453 if matches!(lifecycle_status, TnuaActionLifecycleStatus::CancelledInto) {
454 config.finish_or_reschedule()
455 } else {
456 let landed = B::displacement(ctx.basis)
457 .is_some_and(|displacement| displacement.dot(up) <= 0.0);
458 if landed {
459 config.finish_or_reschedule()
460 } else {
461 let upward_velocity = up.dot(effective_velocity);
462 if upward_velocity <= 0.0 {
463 *memory = TnuaBuiltinJumpMemory::FallSection;
464 continue;
465 }
466
467 let extra_gravity = if config.takeoff_above_velocity <= upward_velocity
468 {
469 config.shorten_extra_gravity + config.takeoff_extra_gravity
470 } else {
471 config.shorten_extra_gravity
472 };
473
474 motor.lin.cancel_on_axis(up);
475 motor.lin.acceleration -= extra_gravity * up;
476 TnuaActionLifecycleDirective::StillActive
477 }
478 }
479 }
480 TnuaBuiltinJumpMemory::FallSection => {
481 let landed = B::displacement(ctx.basis)
482 .is_some_and(|displacement| displacement.dot(up) <= 0.0);
483 if landed
484 || matches!(lifecycle_status, TnuaActionLifecycleStatus::CancelledInto)
485 {
486 config.finish_or_reschedule()
487 } else {
488 motor.lin.cancel_on_axis(up);
489 motor.lin.acceleration -= config.fall_extra_gravity * up;
490 TnuaActionLifecycleDirective::StillActive
491 }
492 }
493 };
494 }
495 error!("Tnua could not decide on jump state");
496 TnuaActionLifecycleDirective::Finished
497 }
498
499 fn influence_basis(
500 &self,
501 _config: &Self::Config,
502 _memory: &Self::Memory,
503 _ctx: crate::TnuaBasisContext,
504 _basis_input: &B,
505 _basis_config: &<B as TnuaBasis>::Config,
506 basis_memory: &mut <B as TnuaBasis>::Memory,
507 ) {
508 B::violate_coyote_time(basis_memory);
509 }
510}