1use std::time::Duration;
2
3use crate::math::{float_consts, AdjustPrecision, AsF32, Float, Quaternion, Vector3};
4use bevy::prelude::*;
5
6use crate::util::rotation_arc_around_axis;
7use crate::TnuaBasisContext;
8use crate::{TnuaBasis, TnuaVelChange};
9
10#[derive(Clone, Debug)]
36pub struct TnuaBuiltinWalk {
37 pub desired_velocity: Vector3,
41
42 pub desired_forward: Option<Dir3>,
47
48 pub float_height: Float,
56
57 pub cling_distance: Float,
64
65 pub spring_strength: Float,
70
71 pub spring_dampening: Float,
79
80 pub acceleration: Float,
86
87 pub air_acceleration: Float,
91
92 pub coyote_time: Float,
94
95 pub free_fall_extra_gravity: Float,
104
105 pub tilt_offset_angvel: Float,
110
111 pub tilt_offset_angacl: Float,
116
117 pub turning_angvel: Float,
119
120 pub max_slope: Float,
122}
123
124impl Default for TnuaBuiltinWalk {
125 fn default() -> Self {
126 Self {
127 desired_velocity: Vector3::ZERO,
128 desired_forward: None,
129 float_height: 0.0,
130 cling_distance: 1.0,
131 spring_strength: 400.0,
132 spring_dampening: 1.2,
133 acceleration: 60.0,
134 air_acceleration: 20.0,
135 coyote_time: 0.15,
136 free_fall_extra_gravity: 60.0,
137 tilt_offset_angvel: 5.0,
138 tilt_offset_angacl: 500.0,
139 turning_angvel: 10.0,
140 max_slope: float_consts::FRAC_PI_2,
141 }
142 }
143}
144
145impl TnuaBasis for TnuaBuiltinWalk {
146 const NAME: &'static str = "TnuaBuiltinWalk";
147 type State = TnuaBuiltinWalkState;
148
149 fn apply(&self, state: &mut Self::State, ctx: TnuaBasisContext, motor: &mut crate::TnuaMotor) {
150 if let Some(stopwatch) = &mut state.airborne_timer {
151 #[allow(clippy::unnecessary_cast)]
152 stopwatch.tick(Duration::from_secs_f64(ctx.frame_duration as f64));
153 }
154
155 let climb_vectors: Option<ClimbVectors>;
156 let considered_in_air: bool;
157 let impulse_to_offset: Vector3;
158 let slipping_vector: Option<Vector3>;
159
160 if let Some(sensor_output) = &ctx.proximity_sensor.output {
161 state.effective_velocity = ctx.tracker.velocity - sensor_output.entity_linvel;
162 let sideways_unnormalized = sensor_output
163 .normal
164 .cross(*ctx.up_direction)
165 .adjust_precision();
166 if sideways_unnormalized == Vector3::ZERO {
167 climb_vectors = None;
168 } else {
169 climb_vectors = Some(ClimbVectors {
170 direction: sideways_unnormalized
171 .cross(sensor_output.normal.adjust_precision())
172 .normalize_or_zero()
173 .adjust_precision(),
174 sideways: sideways_unnormalized.normalize_or_zero().adjust_precision(),
175 });
176 }
177
178 slipping_vector = {
179 let angle_with_floor = sensor_output
180 .normal
181 .angle_between(*ctx.up_direction)
182 .adjust_precision();
183 if angle_with_floor <= self.max_slope {
184 None
185 } else {
186 Some(
187 sensor_output
188 .normal
189 .reject_from(*ctx.up_direction)
190 .adjust_precision(),
191 )
192 }
193 };
194
195 if state.airborne_timer.is_some() {
196 considered_in_air = true;
197 impulse_to_offset = Vector3::ZERO;
198 state.standing_on = None;
199 } else {
200 if let Some(standing_on_state) = &state.standing_on {
201 if standing_on_state.entity != sensor_output.entity {
202 impulse_to_offset = Vector3::ZERO;
203 } else {
204 impulse_to_offset =
205 sensor_output.entity_linvel - standing_on_state.entity_linvel;
206 }
207 } else {
208 impulse_to_offset = Vector3::ZERO;
209 }
210
211 if slipping_vector.is_none() {
212 considered_in_air = false;
213 state.standing_on = Some(StandingOnState {
214 entity: sensor_output.entity,
215 entity_linvel: sensor_output.entity_linvel,
216 });
217 } else {
218 considered_in_air = true;
219 state.standing_on = None;
220 }
221 }
222 } else {
223 state.effective_velocity = ctx.tracker.velocity;
224 climb_vectors = None;
225 considered_in_air = true;
226 impulse_to_offset = Vector3::ZERO;
227 slipping_vector = None;
228 state.standing_on = None;
229 }
230 state.effective_velocity += impulse_to_offset;
231
232 let velocity_on_plane = state
233 .effective_velocity
234 .reject_from(ctx.up_direction.adjust_precision());
235
236 let desired_boost = self.desired_velocity - velocity_on_plane;
237
238 let safe_direction_coefficient = self
239 .desired_velocity
240 .normalize_or_zero()
241 .dot(velocity_on_plane.normalize_or_zero());
242 let direction_change_factor = 1.5 - 0.5 * safe_direction_coefficient;
243
244 let relevant_acceleration_limit = if considered_in_air {
245 self.air_acceleration
246 } else {
247 self.acceleration
248 };
249 let max_acceleration = direction_change_factor * relevant_acceleration_limit;
250
251 state.vertical_velocity = if let Some(climb_vectors) = &climb_vectors {
252 state.effective_velocity.dot(climb_vectors.direction)
253 * climb_vectors
254 .direction
255 .dot(ctx.up_direction.adjust_precision())
256 } else {
257 0.0
258 };
259
260 let walk_vel_change = if self.desired_velocity == Vector3::ZERO && slipping_vector.is_none()
261 {
262 let walk_boost = desired_boost.clamp_length_max(ctx.frame_duration * max_acceleration);
264 let walk_boost = if let Some(climb_vectors) = &climb_vectors {
265 climb_vectors.project(walk_boost)
266 } else {
267 walk_boost
268 };
269 TnuaVelChange::boost(walk_boost)
270 } else {
271 let walk_acceleration =
274 (desired_boost / ctx.frame_duration).clamp_length_max(max_acceleration);
275 let walk_acceleration =
276 if let (Some(climb_vectors), None) = (&climb_vectors, slipping_vector) {
277 climb_vectors.project(walk_acceleration)
278 } else {
279 walk_acceleration
280 };
281
282 let slipping_boost = 'slipping_boost: {
283 let Some(slipping_vector) = slipping_vector else {
284 break 'slipping_boost Vector3::ZERO;
285 };
286 let vertical_velocity = if 0.0 <= state.vertical_velocity {
287 ctx.tracker.gravity.dot(ctx.up_direction.adjust_precision())
288 * ctx.frame_duration
289 } else {
290 state.vertical_velocity
291 };
292
293 let Ok((slipping_direction, slipping_per_vertical_unit)) =
294 Dir3::new_and_length(slipping_vector.f32())
295 else {
296 break 'slipping_boost Vector3::ZERO;
297 };
298
299 let required_veloicty_in_slipping_direction =
300 slipping_per_vertical_unit.adjust_precision() * -vertical_velocity;
301 let expected_velocity = velocity_on_plane + walk_acceleration * ctx.frame_duration;
302 let expected_velocity_in_slipping_direction =
303 expected_velocity.dot(slipping_direction.adjust_precision());
304
305 let diff = required_veloicty_in_slipping_direction
306 - expected_velocity_in_slipping_direction;
307
308 if diff <= 0.0 {
309 break 'slipping_boost Vector3::ZERO;
310 }
311
312 slipping_direction.adjust_precision() * diff
313 };
314 TnuaVelChange {
315 acceleration: walk_acceleration,
316 boost: slipping_boost,
317 }
318 };
319
320 let upward_impulse: TnuaVelChange = 'upward_impulse: {
321 let should_disable_due_to_slipping =
322 slipping_vector.is_some() && state.vertical_velocity <= 0.0;
323 for _ in 0..2 {
324 #[allow(clippy::unnecessary_cast)]
325 match &mut state.airborne_timer {
326 None => {
327 if let (false, Some(sensor_output)) =
328 (should_disable_due_to_slipping, &ctx.proximity_sensor.output)
329 {
330 let spring_offset =
332 self.float_height - sensor_output.proximity.adjust_precision();
333 state.standing_offset =
334 -spring_offset * ctx.up_direction.adjust_precision();
335 break 'upward_impulse self.spring_force(state, &ctx, spring_offset);
336 } else {
337 state.airborne_timer = Some(Timer::from_seconds(
338 self.coyote_time as f32,
339 TimerMode::Once,
340 ));
341 continue;
342 }
343 }
344 Some(_) => {
345 if let (false, Some(sensor_output)) =
346 (should_disable_due_to_slipping, &ctx.proximity_sensor.output)
347 {
348 if sensor_output.proximity.adjust_precision() <= self.float_height {
349 state.airborne_timer = None;
350 continue;
351 }
352 }
353 if state.vertical_velocity <= 0.0 {
354 break 'upward_impulse TnuaVelChange::acceleration(
355 -self.free_fall_extra_gravity * ctx.up_direction.adjust_precision(),
356 );
357 } else {
358 break 'upward_impulse TnuaVelChange::ZERO;
359 }
360 }
361 }
362 }
363 error!("Tnua could not decide on jump state");
364 TnuaVelChange::ZERO
365 };
366
367 motor.lin = walk_vel_change + TnuaVelChange::boost(impulse_to_offset) + upward_impulse;
368 let new_velocity = state.effective_velocity
369 + motor.lin.boost
370 + ctx.frame_duration * motor.lin.acceleration
371 - impulse_to_offset;
372 state.running_velocity = new_velocity.reject_from(ctx.up_direction.adjust_precision());
373
374 let torque_to_fix_tilt = {
377 let tilted_up = ctx.tracker.rotation.mul_vec3(Vector3::Y);
378
379 let rotation_required_to_fix_tilt =
380 Quaternion::from_rotation_arc(tilted_up, ctx.up_direction.adjust_precision());
381
382 let desired_angvel = (rotation_required_to_fix_tilt.xyz() / ctx.frame_duration)
383 .clamp_length_max(self.tilt_offset_angvel);
384 let angular_velocity_diff = desired_angvel - ctx.tracker.angvel;
385 angular_velocity_diff.clamp_length_max(ctx.frame_duration * self.tilt_offset_angacl)
386 };
387
388 let desired_angvel = if let Some(desired_forward) = self.desired_forward {
391 let current_forward = ctx.tracker.rotation.mul_vec3(Vector3::NEG_Z);
392 let rotation_along_up_axis = rotation_arc_around_axis(
393 ctx.up_direction,
394 current_forward,
395 desired_forward.adjust_precision(),
396 )
397 .unwrap_or(0.0);
398 (rotation_along_up_axis / ctx.frame_duration)
399 .clamp(-self.turning_angvel, self.turning_angvel)
400 } else {
401 0.0
402 };
403
404 let existing_angvel = ctx.tracker.angvel.dot(ctx.up_direction.adjust_precision());
406
407 let torque_to_turn = desired_angvel - existing_angvel;
410
411 let existing_turn_torque = torque_to_fix_tilt.dot(ctx.up_direction.adjust_precision());
412 let torque_to_turn = torque_to_turn - existing_turn_torque;
413
414 motor.ang = TnuaVelChange::boost(
415 torque_to_fix_tilt + torque_to_turn * ctx.up_direction.adjust_precision(),
416 );
417 }
418
419 fn proximity_sensor_cast_range(&self, _state: &Self::State) -> Float {
420 self.float_height + self.cling_distance
421 }
422
423 fn displacement(&self, state: &Self::State) -> Option<Vector3> {
424 match state.airborne_timer {
425 None => Some(state.standing_offset),
426 Some(_) => None,
427 }
428 }
429
430 fn effective_velocity(&self, state: &Self::State) -> Vector3 {
431 state.effective_velocity
432 }
433
434 fn vertical_velocity(&self, state: &Self::State) -> Float {
435 state.vertical_velocity
436 }
437
438 fn neutralize(&mut self) {
439 self.desired_velocity = Vector3::ZERO;
440 self.desired_forward = None;
441 }
442
443 fn is_airborne(&self, state: &Self::State) -> bool {
444 state
445 .airborne_timer
446 .as_ref()
447 .is_some_and(|timer| timer.finished())
448 }
449
450 fn violate_coyote_time(&self, state: &mut Self::State) {
451 if let Some(timer) = &mut state.airborne_timer {
452 timer.set_duration(Duration::ZERO);
453 }
454 }
455}
456
457impl TnuaBuiltinWalk {
458 pub fn spring_force(
465 &self,
466 state: &TnuaBuiltinWalkState,
467 ctx: &TnuaBasisContext,
468 spring_offset: Float,
469 ) -> TnuaVelChange {
470 let spring_force: Float = spring_offset * self.spring_strength;
471
472 let relative_velocity = state
473 .effective_velocity
474 .dot(ctx.up_direction.adjust_precision())
475 - state.vertical_velocity;
476
477 let gravity_compensation = -ctx.tracker.gravity;
478
479 let dampening_boost = relative_velocity * self.spring_dampening;
480
481 TnuaVelChange {
482 acceleration: ctx.up_direction.adjust_precision() * spring_force + gravity_compensation,
483 boost: ctx.up_direction.adjust_precision() * -dampening_boost,
484 }
485 }
486}
487
488#[derive(Debug, Clone)]
489struct StandingOnState {
490 entity: Entity,
491 entity_linvel: Vector3,
492}
493
494#[derive(Default, Clone, Debug)]
495pub struct TnuaBuiltinWalkState {
496 airborne_timer: Option<Timer>,
497 pub standing_offset: Vector3,
499 standing_on: Option<StandingOnState>,
500 effective_velocity: Vector3,
501 vertical_velocity: Float,
502 pub running_velocity: Vector3,
508}
509
510impl TnuaBuiltinWalkState {
511 pub fn standing_on_entity(&self) -> Option<Entity> {
513 Some(self.standing_on.as_ref()?.entity)
514 }
515
516 pub fn reset_airborne_timer(&mut self) {
517 self.airborne_timer = None;
518 }
519}
520
521#[derive(Debug, Clone)]
522struct ClimbVectors {
523 direction: Vector3,
524 sideways: Vector3,
525}
526
527impl ClimbVectors {
528 fn project(&self, vector: Vector3) -> Vector3 {
529 let axis_direction = vector.dot(self.direction) * self.direction;
530 let axis_sideways = vector.dot(self.sideways) * self.sideways;
531 axis_direction + axis_sideways
532 }
533}