1use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
2use bevy::platform::collections::hash_map::Entry;
3use bevy::platform::collections::HashMap;
4use bevy::prelude::*;
5use bevy::time::Stopwatch;
6use bevy_tnua_physics_integration_layer::math::{
7 AdjustPrecision, AsF32, Float, Quaternion, Vector3,
8};
9
10use crate::basis_action_traits::{
11 BoxableAction, BoxableBasis, DynamicAction, DynamicBasis, TnuaAction, TnuaActionContext,
12 TnuaActionInitiationDirective, TnuaActionLifecycleDirective, TnuaActionLifecycleStatus,
13 TnuaBasisContext,
14};
15use crate::{
16 TnuaBasis, TnuaMotor, TnuaPipelineStages, TnuaProximitySensor, TnuaRigidBodyTracker,
17 TnuaSystemSet, TnuaToggle, TnuaUserControlsSystemSet,
18};
19
20pub struct TnuaControllerPlugin {
32 schedule: InternedScheduleLabel,
33}
34
35impl TnuaControllerPlugin {
36 pub fn new(schedule: impl ScheduleLabel) -> Self {
37 Self {
38 schedule: schedule.intern(),
39 }
40 }
41}
42
43impl Plugin for TnuaControllerPlugin {
44 fn build(&self, app: &mut App) {
45 app.configure_sets(
46 self.schedule,
47 (
48 TnuaPipelineStages::Sensors,
49 TnuaPipelineStages::SubservientSensors,
50 TnuaUserControlsSystemSet,
51 TnuaPipelineStages::Logic,
52 TnuaPipelineStages::Motors,
53 )
54 .chain()
55 .in_set(TnuaSystemSet),
56 );
57 app.add_systems(
58 self.schedule,
59 apply_controller_system.in_set(TnuaPipelineStages::Logic),
60 );
61 }
62}
63
64struct FedEntry {
65 fed_this_frame: bool,
66 rescheduled_in: Option<Timer>,
67}
68
69#[derive(Component, Default)]
92#[require(TnuaMotor, TnuaRigidBodyTracker, TnuaProximitySensor)]
93pub struct TnuaController {
94 current_basis: Option<(&'static str, Box<dyn DynamicBasis>)>,
95 actions_being_fed: HashMap<&'static str, FedEntry>,
96 current_action: Option<(&'static str, Box<dyn DynamicAction>)>,
97 contender_action: Option<(&'static str, Box<dyn DynamicAction>, Stopwatch)>,
98 action_flow_status: TnuaActionFlowStatus,
99 up_direction: Option<Dir3>,
100}
101
102impl TnuaController {
103 pub fn basis<B: TnuaBasis>(&mut self, basis: B) {
105 self.named_basis(B::NAME, basis);
106 }
107
108 pub fn named_basis<B: TnuaBasis>(&mut self, name: &'static str, basis: B) {
114 if let Some((existing_name, existing_basis)) =
115 self.current_basis.as_mut().and_then(|(n, b)| {
116 let b = b.as_mut_any().downcast_mut::<BoxableBasis<B>>()?;
117 Some((n, b))
118 })
119 {
120 *existing_name = name;
121 existing_basis.input = basis;
122 } else {
123 self.current_basis = Some((name, Box::new(BoxableBasis::new(basis))));
124 }
125 }
126
127 pub fn neutralize_basis(&mut self) {
133 if let Some((_, basis)) = self.current_basis.as_mut() {
134 basis.neutralize();
135 }
136 }
137
138 pub fn basis_name(&self) -> Option<&'static str> {
143 self.current_basis
144 .as_ref()
145 .map(|(basis_name, _)| *basis_name)
146 }
147
148 pub fn dynamic_basis(&self) -> Option<&dyn DynamicBasis> {
150 Some(self.current_basis.as_ref()?.1.as_ref())
151 }
152
153 pub fn concrete_basis<B: TnuaBasis>(&self) -> Option<(&B, &B::State)> {
159 let (_, basis) = self.current_basis.as_ref()?;
160 let boxable_basis: &BoxableBasis<B> = basis.as_any().downcast_ref()?;
161 Some((&boxable_basis.input, &boxable_basis.state))
162 }
163
164 pub fn concrete_basis_mut<B: TnuaBasis>(&mut self) -> Option<(&B, &mut B::State)> {
167 let (_, basis) = self.current_basis.as_mut()?;
168 let boxable_basis: &mut BoxableBasis<B> = basis.as_mut_any().downcast_mut()?;
169 Some((&boxable_basis.input, &mut boxable_basis.state))
170 }
171
172 pub fn action<A: TnuaAction>(&mut self, action: A) {
174 self.named_action(A::NAME, action);
175 }
176
177 pub fn named_action<A: TnuaAction>(&mut self, name: &'static str, action: A) {
183 match self.actions_being_fed.entry(name) {
184 Entry::Occupied(mut entry) => {
185 entry.get_mut().fed_this_frame = true;
186 if let Some((current_name, current_action)) = self.current_action.as_mut() {
187 if *current_name == name {
188 let Some(current_action) = current_action
189 .as_mut_any()
190 .downcast_mut::<BoxableAction<A>>()
191 else {
192 panic!("Multiple action types registered with same name {name:?}");
193 };
194 current_action.input = action;
195 } else {
196 }
199 } else if self.contender_action.is_none()
200 && entry
201 .get()
202 .rescheduled_in
203 .as_ref()
204 .is_some_and(|timer| timer.finished())
205 {
206 self.contender_action =
209 Some((name, Box::new(BoxableAction::new(action)), Stopwatch::new()));
210 } else {
211 }
213 }
214 Entry::Vacant(entry) => {
215 entry.insert(FedEntry {
216 fed_this_frame: true,
217 rescheduled_in: None,
218 });
219 if let Some(contender_action) = self.contender_action.as_mut().and_then(
220 |(contender_name, contender_action, _)| {
221 if *contender_name == name {
222 let Some(contender_action) = contender_action
223 .as_mut_any()
224 .downcast_mut::<BoxableAction<A>>()
225 else {
226 panic!("Multiple action types registered with same name {name:?}");
227 };
228 Some(contender_action)
229 } else {
230 None
231 }
232 },
233 ) {
234 contender_action.input = action;
235 } else {
236 self.contender_action =
237 Some((name, Box::new(BoxableAction::new(action)), Stopwatch::new()));
238 }
239 }
240 }
241 }
242
243 pub fn prolong_action(&mut self) {
249 if let Some((current_name, _)) = self.current_action {
250 if let Some(fed_action) = self.actions_being_fed.get_mut(current_name) {
251 fed_action.fed_this_frame = true;
252 }
253 }
254 }
255
256 pub fn action_name(&self) -> Option<&'static str> {
261 self.current_action
262 .as_ref()
263 .map(|(action_name, _)| *action_name)
264 }
265
266 pub fn dynamic_action(&self) -> Option<&dyn DynamicAction> {
268 Some(self.current_action.as_ref()?.1.as_ref())
269 }
270
271 pub fn concrete_action<A: TnuaAction>(&self) -> Option<(&A, &A::State)> {
277 let (_, action) = self.current_action.as_ref()?;
278 let boxable_action: &BoxableAction<A> = action.as_any().downcast_ref()?;
279 Some((&boxable_action.input, &boxable_action.state))
280 }
281
282 pub fn concrete_action_mut<A: TnuaAction>(&mut self) -> Option<(&A, &mut A::State)> {
288 let (_, action) = self.current_action.as_mut()?;
289 let boxable_action: &mut BoxableAction<A> = action.as_mut_any().downcast_mut()?;
290 Some((&boxable_action.input, &mut boxable_action.state))
291 }
292
293 pub fn action_flow_status(&self) -> &TnuaActionFlowStatus {
306 &self.action_flow_status
307 }
308
309 pub fn is_airborne(&self) -> Result<bool, TnuaControllerHasNoBasis> {
315 match self.dynamic_basis() {
316 Some(basis) => Ok(basis.is_airborne()),
317 None => Err(TnuaControllerHasNoBasis),
318 }
319 }
320
321 pub fn up_direction(&self) -> Option<Dir3> {
329 self.up_direction
330 }
331}
332
333#[derive(thiserror::Error, Debug)]
334#[error("The Tnua controller does not have any basis set")]
335pub struct TnuaControllerHasNoBasis;
336
337#[derive(Debug, Default, Clone)]
339pub enum TnuaActionFlowStatus {
340 #[default]
342 NoAction,
343
344 ActionStarted(&'static str),
346
347 ActionOngoing(&'static str),
349
350 ActionEnded(&'static str),
354
355 Cancelled {
357 old: &'static str,
358 new: &'static str,
359 },
360}
361
362impl TnuaActionFlowStatus {
363 pub fn ongoing(&self) -> Option<&'static str> {
367 match self {
368 TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionEnded(_) => None,
369 TnuaActionFlowStatus::ActionStarted(action_name)
370 | TnuaActionFlowStatus::ActionOngoing(action_name)
371 | TnuaActionFlowStatus::Cancelled {
372 old: _,
373 new: action_name,
374 } => Some(action_name),
375 }
376 }
377
378 pub fn just_starting(&self) -> Option<&'static str> {
383 match self {
384 TnuaActionFlowStatus::NoAction
385 | TnuaActionFlowStatus::ActionOngoing(_)
386 | TnuaActionFlowStatus::ActionEnded(_) => None,
387 TnuaActionFlowStatus::ActionStarted(action_name)
388 | TnuaActionFlowStatus::Cancelled {
389 old: _,
390 new: action_name,
391 } => Some(action_name),
392 }
393 }
394}
395
396#[allow(clippy::type_complexity)]
397fn apply_controller_system(
398 time: Res<Time>,
399 mut query: Query<(
400 &mut TnuaController,
401 &TnuaRigidBodyTracker,
402 &mut TnuaProximitySensor,
403 &mut TnuaMotor,
404 Option<&TnuaToggle>,
405 )>,
406) {
407 let frame_duration = time.delta().as_secs_f64() as Float;
408 if frame_duration == 0.0 {
409 return;
410 }
411 for (mut controller, tracker, mut sensor, mut motor, tnua_toggle) in query.iter_mut() {
412 match tnua_toggle.copied().unwrap_or_default() {
413 TnuaToggle::Disabled => continue,
414 TnuaToggle::SenseOnly => {}
415 TnuaToggle::Enabled => {}
416 }
417
418 let controller = controller.as_mut();
419
420 let up_direction = Dir3::new(-tracker.gravity.f32()).ok();
421 controller.up_direction = up_direction;
422 let up_direction = up_direction.unwrap_or(Dir3::Y);
424
425 match controller.action_flow_status {
426 TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionOngoing(_) => {}
427 TnuaActionFlowStatus::ActionEnded(_) => {
428 controller.action_flow_status = TnuaActionFlowStatus::NoAction;
429 }
430 TnuaActionFlowStatus::ActionStarted(action_name)
431 | TnuaActionFlowStatus::Cancelled {
432 old: _,
433 new: action_name,
434 } => {
435 controller.action_flow_status = TnuaActionFlowStatus::ActionOngoing(action_name);
436 }
437 }
438
439 if let Some((_, basis)) = controller.current_basis.as_mut() {
440 let basis = basis.as_mut();
441 basis.apply(
442 TnuaBasisContext {
443 frame_duration,
444 tracker,
445 proximity_sensor: sensor.as_ref(),
446 up_direction,
447 },
448 motor.as_mut(),
449 );
450 let sensor_cast_range_for_basis = basis.proximity_sensor_cast_range();
451
452 let proximity_sensor = sensor.as_ref();
454
455 let has_valid_contender = if let Some((_, contender_action, being_fed_for)) =
456 &mut controller.contender_action
457 {
458 let initiation_decision = contender_action.initiation_decision(
459 TnuaActionContext {
460 frame_duration,
461 tracker,
462 proximity_sensor,
463 basis,
464 up_direction,
465 },
466 being_fed_for,
467 );
468 being_fed_for.tick(time.delta());
469 match initiation_decision {
470 TnuaActionInitiationDirective::Reject => {
471 controller.contender_action = None;
472 false
473 }
474 TnuaActionInitiationDirective::Delay => false,
475 TnuaActionInitiationDirective::Allow => true,
476 }
477 } else {
478 false
479 };
480
481 if let Some((name, current_action)) = controller.current_action.as_mut() {
482 let lifecycle_status = if has_valid_contender {
483 TnuaActionLifecycleStatus::CancelledInto
484 } else if controller
485 .actions_being_fed
486 .get(name)
487 .map(|fed_entry| fed_entry.fed_this_frame)
488 .unwrap_or(false)
489 {
490 TnuaActionLifecycleStatus::StillFed
491 } else {
492 TnuaActionLifecycleStatus::NoLongerFed
493 };
494
495 let directive = current_action.apply(
496 TnuaActionContext {
497 frame_duration,
498 tracker,
499 proximity_sensor,
500 basis,
501 up_direction,
502 },
503 lifecycle_status,
504 motor.as_mut(),
505 );
506 if current_action.violates_coyote_time() {
507 basis.violate_coyote_time();
508 }
509 let reschedule_action =
510 |actions_being_fed: &mut HashMap<&'static str, FedEntry>,
511 after_seconds: Float| {
512 if let Some(fed_entry) = actions_being_fed.get_mut(name) {
513 fed_entry.rescheduled_in =
514 Some(Timer::from_seconds(after_seconds.f32(), TimerMode::Once));
515 }
516 };
517 match directive {
518 TnuaActionLifecycleDirective::StillActive => {
519 if !lifecycle_status.is_active()
520 && matches!(
521 controller.action_flow_status,
522 TnuaActionFlowStatus::ActionOngoing(_)
523 )
524 {
525 controller.action_flow_status = TnuaActionFlowStatus::ActionEnded(name);
526 }
527 }
528 TnuaActionLifecycleDirective::Finished
529 | TnuaActionLifecycleDirective::Reschedule { .. } => {
530 if let TnuaActionLifecycleDirective::Reschedule { after_seconds } =
531 directive
532 {
533 reschedule_action(&mut controller.actions_being_fed, after_seconds);
534 }
535 controller.current_action = if has_valid_contender {
536 let (contender_name, mut contender_action, _) = controller.contender_action.take().expect("has_valid_contender can only be true if contender_action is Some");
537 if let Some(contender_fed_entry) =
538 controller.actions_being_fed.get_mut(contender_name)
539 {
540 contender_fed_entry.rescheduled_in = None;
541 }
542 let contender_directive = contender_action.apply(
543 TnuaActionContext {
544 frame_duration,
545 tracker,
546 proximity_sensor,
547 basis,
548 up_direction,
549 },
550 TnuaActionLifecycleStatus::CancelledFrom,
551 motor.as_mut(),
552 );
553 if contender_action.violates_coyote_time() {
554 basis.violate_coyote_time();
555 }
556 match contender_directive {
557 TnuaActionLifecycleDirective::StillActive => {
558 if matches!(
559 controller.action_flow_status,
560 TnuaActionFlowStatus::ActionOngoing(_)
561 ) {
562 controller.action_flow_status =
563 TnuaActionFlowStatus::Cancelled {
564 old: name,
565 new: contender_name,
566 };
567 } else {
568 controller.action_flow_status =
569 TnuaActionFlowStatus::ActionStarted(contender_name);
570 }
571 Some((contender_name, contender_action))
572 }
573 TnuaActionLifecycleDirective::Finished => {
574 if matches!(
575 controller.action_flow_status,
576 TnuaActionFlowStatus::ActionOngoing(_)
577 ) {
578 controller.action_flow_status =
579 TnuaActionFlowStatus::ActionEnded(name);
580 }
581 None
582 }
583 TnuaActionLifecycleDirective::Reschedule { after_seconds } => {
584 if matches!(
585 controller.action_flow_status,
586 TnuaActionFlowStatus::ActionOngoing(_)
587 ) {
588 controller.action_flow_status =
589 TnuaActionFlowStatus::ActionEnded(name);
590 }
591 reschedule_action(
592 &mut controller.actions_being_fed,
593 after_seconds,
594 );
595 None
596 }
597 }
598 } else {
599 controller.action_flow_status = TnuaActionFlowStatus::ActionEnded(name);
600 None
601 };
602 }
603 }
604 } else if has_valid_contender {
605 let (contender_name, mut contender_action, _) = controller
606 .contender_action
607 .take()
608 .expect("has_valid_contender can only be true if contender_action is Some");
609 contender_action.apply(
610 TnuaActionContext {
611 frame_duration,
612 tracker,
613 proximity_sensor,
614 basis,
615 up_direction,
616 },
617 TnuaActionLifecycleStatus::Initiated,
618 motor.as_mut(),
619 );
620 if contender_action.violates_coyote_time() {
621 basis.violate_coyote_time();
622 }
623 controller.action_flow_status = TnuaActionFlowStatus::ActionStarted(contender_name);
624 controller.current_action = Some((contender_name, contender_action));
625 }
626
627 let sensor_case_range_for_action =
628 if let Some((_, current_action)) = &controller.current_action {
629 current_action.proximity_sensor_cast_range()
630 } else {
631 0.0
632 };
633
634 sensor.cast_range = sensor_cast_range_for_basis.max(sensor_case_range_for_action);
635 sensor.cast_direction = -up_direction;
636 sensor.cast_shape_rotation =
638 Quaternion::from_rotation_arc(Vector3::Y, up_direction.adjust_precision())
639 }
640
641 controller.actions_being_fed.retain(|_, fed_entry| {
643 if fed_entry.fed_this_frame {
644 fed_entry.fed_this_frame = false;
645 if let Some(rescheduled_in) = &mut fed_entry.rescheduled_in {
646 rescheduled_in.tick(time.delta());
647 }
648 true
649 } else {
650 false
651 }
652 });
653
654 if let Some((contender_name, ..)) = controller.contender_action {
655 if !controller.actions_being_fed.contains_key(contender_name) {
656 controller.contender_action = None;
657 }
658 }
659 }
660}