bevy_landmass/
agent.rs

1use std::{marker::PhantomData, ops::Deref};
2
3use bevy_ecs::query::Has;
4use bevy_ecs::system::Commands;
5use bevy_ecs::{
6  bundle::Bundle, change_detection::DetectChanges, component::Component,
7  entity::Entity, query::With, system::Query, world::Ref,
8};
9use bevy_log::warn_once;
10use bevy_platform::collections::HashMap;
11use bevy_transform::{components::Transform, helper::TransformHelper};
12use landmass::AnimationLinkId;
13
14use crate::{
15  AgentState, Archipelago, TargetReachedCondition, Velocity,
16  coords::{CoordinateSystem, ThreeD, TwoD},
17};
18use crate::{ArchipelagoRef, PermittedAnimationLinks};
19
20/// A bundle to create agents. This omits the GlobalTransform component, since
21/// this is commonly added in other bundles (which is redundant and can override
22/// previous bundles).
23#[derive(Bundle)]
24pub struct AgentBundle<CS: CoordinateSystem> {
25  /// The agent marker.
26  pub agent: Agent<CS>,
27  /// The agent's settings.
28  pub settings: AgentSettings,
29  /// A reference pointing to the Archipelago to associate this entity with.
30  pub archipelago_ref: ArchipelagoRef<CS>,
31}
32
33pub type Agent2dBundle = AgentBundle<TwoD>;
34pub type Agent3dBundle = AgentBundle<ThreeD>;
35
36/// A marker component to create all required components for an agent.
37#[derive(Component)]
38#[require(Transform, Velocity<CS>, AgentTarget<CS>, AgentState, AgentDesiredVelocity<CS>)]
39pub struct Agent<CS: CoordinateSystem>(PhantomData<CS>);
40
41pub type Agent2d = Agent<TwoD>;
42pub type Agent3d = Agent<ThreeD>;
43
44impl<CS: CoordinateSystem> Default for Agent<CS> {
45  fn default() -> Self {
46    Self(Default::default())
47  }
48}
49
50/// The settings for an agent. See [`crate::AgentBundle`] for required related
51/// components.
52#[derive(Component, Debug)]
53pub struct AgentSettings {
54  /// The radius of the agent.
55  pub radius: f32,
56  /// The speed the agent prefers to move at. This should often be set lower
57  /// than the [`Self::max_speed`] to allow the agent to "speed up" in order to
58  /// get out of another agent's way.
59  pub desired_speed: f32,
60  /// The max speed of an agent.
61  pub max_speed: f32,
62}
63
64/// The distance at which an animation link can be used.
65///
66/// If not present on an agent, this will use the same distance as
67/// [`TargetReachedCondition`] but behaving like
68/// [`TargetReachedCondition::StraightPathDistance`].
69#[derive(Component, Debug)]
70pub struct AnimationLinkReachedDistance(pub f32);
71
72#[derive(Component, Default, Debug)]
73pub struct AgentTypeIndexCostOverrides(HashMap<usize, f32>);
74
75impl Deref for AgentTypeIndexCostOverrides {
76  type Target = HashMap<usize, f32>;
77  fn deref(&self) -> &Self::Target {
78    &self.0
79  }
80}
81
82impl AgentTypeIndexCostOverrides {
83  /// Sets the type index cost for this agent to `cost`. Returns false if the
84  /// cost is <= 0.0. Otherwise returns true.
85  pub fn set_type_index_cost(&mut self, type_index: usize, cost: f32) -> bool {
86    if cost <= 0.0 {
87      return false;
88    }
89    self.0.insert(type_index, cost);
90    true
91  }
92}
93
94/// The current target of the entity. Note this can be set by either reinserting
95/// the component, or dereferencing:
96///
97/// ```rust
98/// # use bevy::prelude::*;
99/// # use bevy_landmass::AgentTarget3d;
100/// fn clear_targets(mut targets: Query<&mut AgentTarget3d>) {
101///   for mut target in targets.iter_mut() {
102///     *target = AgentTarget3d::None;
103///   }
104/// }
105/// ```
106#[derive(Component)]
107pub enum AgentTarget<CS: CoordinateSystem> {
108  None,
109  Point(CS::Coordinate),
110  Entity(Entity),
111}
112
113pub type AgentTarget2d = AgentTarget<TwoD>;
114pub type AgentTarget3d = AgentTarget<ThreeD>;
115
116impl<CS: CoordinateSystem> Default for AgentTarget<CS> {
117  fn default() -> Self {
118    Self::None
119  }
120}
121
122impl<CS: CoordinateSystem<Coordinate: std::fmt::Debug>> std::fmt::Debug
123  for AgentTarget<CS>
124{
125  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126    match self {
127      Self::None => write!(f, "None"),
128      Self::Point(arg0) => f.debug_tuple("Point").field(arg0).finish(),
129      Self::Entity(arg0) => f.debug_tuple("Entity").field(arg0).finish(),
130    }
131  }
132}
133
134impl<CS: CoordinateSystem<Coordinate: PartialEq>> PartialEq
135  for AgentTarget<CS>
136{
137  fn eq(&self, other: &Self) -> bool {
138    match (self, other) {
139      (Self::Point(l0), Self::Point(r0)) => l0 == r0,
140      (Self::Entity(l0), Self::Entity(r0)) => l0 == r0,
141      _ => core::mem::discriminant(self) == core::mem::discriminant(other),
142    }
143  }
144}
145
146impl<CS: CoordinateSystem<Coordinate: Eq>> Eq for AgentTarget<CS> {}
147
148impl<CS: CoordinateSystem> AgentTarget<CS> {
149  /// Converts an agent target to a concrete world position.
150  fn to_point(
151    &self,
152    transform_helper: &TransformHelper,
153  ) -> Option<CS::Coordinate> {
154    match self {
155      Self::Point(point) => Some(point.clone()),
156      &Self::Entity(entity) => transform_helper
157        .compute_global_transform(entity)
158        .ok()
159        .map(|transform| CS::from_bevy_position(transform.translation())),
160      _ => None,
161    }
162  }
163}
164
165/// The current desired velocity of the agent. This is set by `landmass` (during
166/// [`crate::LandmassSystemSet::Output`]).
167#[derive(Component)]
168pub struct AgentDesiredVelocity<CS: CoordinateSystem>(CS::Coordinate);
169
170pub type AgentDesiredVelocity2d = AgentDesiredVelocity<TwoD>;
171pub type AgentDesiredVelocity3d = AgentDesiredVelocity<ThreeD>;
172
173impl<CS: CoordinateSystem> Default for AgentDesiredVelocity<CS> {
174  fn default() -> Self {
175    Self(Default::default())
176  }
177}
178
179impl<CS: CoordinateSystem<Coordinate: std::fmt::Debug>> std::fmt::Debug
180  for AgentDesiredVelocity<CS>
181{
182  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183    f.debug_tuple("AgentDesiredVelocity").field(&self.0).finish()
184  }
185}
186
187impl<CS: CoordinateSystem> AgentDesiredVelocity<CS> {
188  /// The desired velocity of the agent.
189  pub fn velocity(&self) -> CS::Coordinate {
190    self.0.clone()
191  }
192}
193
194/// An animation link that an agent has reached (in order to use it).
195#[derive(Component)]
196pub struct ReachedAnimationLink<CS: CoordinateSystem> {
197  /// The ID of the animation link.
198  pub link_entity: Entity,
199  /// The point that the animation link starts at.
200  pub start_point: CS::Coordinate,
201  /// The expected point that using the animation link will take the agent to.
202  pub end_point: CS::Coordinate,
203}
204
205pub type ReachedAnimationLink2d = ReachedAnimationLink<TwoD>;
206pub type ReachedAnimationLink3d = ReachedAnimationLink<ThreeD>;
207
208impl<CS: CoordinateSystem<Coordinate: std::fmt::Debug>> std::fmt::Debug
209  for ReachedAnimationLink<CS>
210{
211  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212    f.debug_struct("ReachedAnimationLink")
213      .field("link_entity", &self.link_entity)
214      .field("start_point", &self.start_point)
215      .field("end_point", &self.end_point)
216      .finish()
217  }
218}
219
220/// A marker component to indicate that an agent should be paused.
221///
222/// Paused agents are not considered for avoidance, and will not recompute their
223/// paths. However, their paths are still kept "consistent" - meaning that once
224/// the agent becomes unpaused, it can reuse that path if it is still valid and
225/// relevant (the agent still wants to go to the same place).
226#[derive(Component, Default, Clone, Copy, Debug)]
227pub struct PauseAgent;
228
229/// A marker component to indicate that an agent is currently using an animation
230/// link and should behave as though it is paused (see [`PauseAgent`] for
231/// details).
232#[derive(Component, Default, Clone, Copy, Debug)]
233pub struct UsingAnimationLink;
234
235#[cfg(feature = "debug-avoidance")]
236/// If inserted on an agent, it will record avoidance data that can later be
237/// visualized with [`crate::debug::draw_avoidance_data`].
238#[derive(Component, Clone, Copy, Debug)]
239pub struct KeepAvoidanceData;
240
241/// Ensures every Bevy agent has a corresponding `landmass` agent.
242pub(crate) fn add_agents_to_archipelagos<CS: CoordinateSystem>(
243  mut archipelago_query: Query<(Entity, &mut Archipelago<CS>)>,
244  agent_query: Query<
245    (Entity, &AgentSettings, &ArchipelagoRef<CS>),
246    With<Transform>,
247  >,
248) {
249  let mut archipelago_to_agents = HashMap::<_, HashMap<_, _>>::default();
250  for (entity, agent, archipleago_ref) in agent_query.iter() {
251    archipelago_to_agents
252      .entry(archipleago_ref.entity)
253      .or_default()
254      .insert(entity, agent);
255  }
256
257  for (archipelago_entity, mut archipelago) in archipelago_query.iter_mut() {
258    let mut new_agent_map = archipelago_to_agents
259      .remove(&archipelago_entity)
260      .unwrap_or_else(HashMap::default);
261    let archipelago = archipelago.as_mut();
262
263    // Remove any agents that aren't in the `new_agent_map`. Also remove any
264    // agents from the `new_agent_map` that are in the archipelago.
265    archipelago.agents.retain(|agent_entity, agent_id| {
266      match new_agent_map.remove(agent_entity) {
267        None => {
268          archipelago.archipelago.remove_agent(*agent_id);
269          archipelago.reverse_agents.remove(agent_id);
270          false
271        }
272        Some(_) => true,
273      }
274    });
275
276    for (new_agent_entity, new_agent) in new_agent_map.drain() {
277      let agent_id =
278        archipelago.archipelago.add_agent(landmass::Agent::create(
279          /* position= */ CS::from_landmass(&landmass::Vec3::ZERO),
280          /* velocity= */ CS::from_landmass(&landmass::Vec3::ZERO),
281          new_agent.radius,
282          new_agent.desired_speed,
283          new_agent.max_speed,
284        ));
285      archipelago.agents.insert(new_agent_entity, agent_id);
286      archipelago.reverse_agents.insert(agent_id, new_agent_entity);
287    }
288  }
289}
290
291#[cfg(feature = "debug-avoidance")]
292type HasKeepAvoidanceData = Has<KeepAvoidanceData>;
293#[cfg(not(feature = "debug-avoidance"))]
294type HasKeepAvoidanceData = ();
295
296/// Ensures the "input state" (position, velocity, etc) of every Bevy agent
297/// matches its `landmass` counterpart.
298pub(crate) fn sync_agent_input_state<CS: CoordinateSystem>(
299  agent_query: Query<
300    (
301      Entity,
302      &AgentSettings,
303      &ArchipelagoRef<CS>,
304      Option<&Velocity<CS>>,
305      Option<&AgentTarget<CS>>,
306      Option<&TargetReachedCondition>,
307      Option<&AnimationLinkReachedDistance>,
308      Option<&PermittedAnimationLinks>,
309      Option<Ref<AgentTypeIndexCostOverrides>>,
310      Has<PauseAgent>,
311      Has<UsingAnimationLink>,
312      HasKeepAvoidanceData,
313    ),
314    With<Transform>,
315  >,
316  transform_helper: TransformHelper,
317  mut archipelago_query: Query<&mut Archipelago<CS>>,
318) {
319  for (
320    agent_entity,
321    agent,
322    &ArchipelagoRef { entity: arch_entity, .. },
323    velocity,
324    target,
325    target_reached_condition,
326    animation_link_reached_distance,
327    permitted_animation_links,
328    type_index_cost_overrides,
329    has_pause_agent,
330    has_using_animation_link,
331    keep_avoidance_data,
332  ) in agent_query.iter()
333  {
334    let mut archipelago = match archipelago_query.get_mut(arch_entity) {
335      Err(_) => continue,
336      Ok(arch) => arch,
337    };
338
339    let Ok(transform) = transform_helper.compute_global_transform(agent_entity)
340    else {
341      continue;
342    };
343
344    let landmass_agent = archipelago
345      .get_agent_mut(agent_entity)
346      .expect("this agent is in the archipelago");
347    landmass_agent.position = CS::from_bevy_position(transform.translation());
348    if let Some(Velocity { velocity }) = velocity {
349      landmass_agent.velocity = velocity.clone();
350    }
351    landmass_agent.radius = agent.radius;
352    landmass_agent.desired_speed = agent.desired_speed;
353    landmass_agent.max_speed = agent.max_speed;
354    landmass_agent.current_target =
355      target.and_then(|target| target.to_point(&transform_helper));
356    landmass_agent.target_reached_condition =
357      if let Some(target_reached_condition) = target_reached_condition {
358        target_reached_condition.to_landmass()
359      } else {
360        landmass::TargetReachedCondition::Distance(None)
361      };
362    landmass_agent.animation_link_reached_distance =
363      animation_link_reached_distance.map(|distance| distance.0);
364    landmass_agent.permitted_animation_links = permitted_animation_links
365      .map(PermittedAnimationLinks::to_landmass)
366      .unwrap_or(landmass::PermittedAnimationLinks::All);
367    match type_index_cost_overrides {
368      None => {
369        for (type_index, _) in
370          landmass_agent.get_type_index_cost_overrides().collect::<Vec<_>>()
371        {
372          landmass_agent.remove_overridden_type_index_cost(type_index);
373        }
374      }
375      Some(type_index_cost_overrides) => {
376        if !type_index_cost_overrides.is_changed() {
377          continue;
378        }
379
380        for (type_index, _) in
381          landmass_agent.get_type_index_cost_overrides().collect::<Vec<_>>()
382        {
383          if type_index_cost_overrides.0.contains_key(&type_index) {
384            continue;
385          }
386          landmass_agent.remove_overridden_type_index_cost(type_index);
387        }
388
389        for (&type_index, &cost) in type_index_cost_overrides.0.iter() {
390          assert!(landmass_agent.override_type_index_cost(type_index, cost));
391        }
392      }
393    }
394    landmass_agent.paused = has_pause_agent;
395    match (landmass_agent.is_using_animation_link(), has_using_animation_link) {
396      (true, false) => {
397        landmass_agent.end_animation_link().unwrap();
398      }
399      (false, true) => match landmass_agent.start_animation_link() {
400        Ok(()) => {}
401        Err(err) => {
402          warn_once!("Failed to start animation link: {err}");
403        }
404      },
405      (true, true) | (false, false) => {}
406    }
407
408    #[cfg(feature = "debug-avoidance")]
409    {
410      landmass_agent.keep_avoidance_data = keep_avoidance_data;
411    }
412    #[cfg(not(feature = "debug-avoidance"))]
413    #[expect(clippy::let_unit_value)]
414    let _ = keep_avoidance_data;
415  }
416}
417
418/// Copies the agent state from `landmass` agents to their Bevy equivalent.
419pub(crate) fn sync_agent_state<CS: CoordinateSystem>(
420  mut agent_query: Query<
421    (Entity, &ArchipelagoRef<CS>, &mut AgentState),
422    With<AgentSettings>,
423  >,
424  archipelago_query: Query<&Archipelago<CS>>,
425) {
426  for (agent_entity, &ArchipelagoRef { entity: arch_entity, .. }, mut state) in
427    agent_query.iter_mut()
428  {
429    let archipelago = match archipelago_query.get(arch_entity).ok() {
430      None => continue,
431      Some(arch) => arch,
432    };
433
434    *state = AgentState::from_landmass(
435      &archipelago
436        .get_agent(agent_entity)
437        .expect("the agent is in the archipelago")
438        .state(),
439    );
440  }
441}
442
443/// Copies the agent desired velocity from `landmass` agents to their Bevy
444/// equivalent.
445pub(crate) fn sync_desired_velocity<CS: CoordinateSystem>(
446  mut agent_query: Query<
447    (Entity, &ArchipelagoRef<CS>, &mut AgentDesiredVelocity<CS>),
448    With<AgentSettings>,
449  >,
450  archipelago_query: Query<&Archipelago<CS>>,
451) {
452  for (
453    agent_entity,
454    &ArchipelagoRef { entity: arch_entity, .. },
455    mut desired_velocity,
456  ) in agent_query.iter_mut()
457  {
458    let archipelago = match archipelago_query.get(arch_entity).ok() {
459      None => continue,
460      Some(arch) => arch,
461    };
462
463    desired_velocity.0 = archipelago
464      .get_agent(agent_entity)
465      .expect("the agent is in the archipelago")
466      .get_desired_velocity()
467      .clone();
468  }
469}
470
471impl<CS: CoordinateSystem> ReachedAnimationLink<CS> {
472  /// Converts the `landmass` representation of the reached animation link, to
473  /// the `bevy_landmass` version.
474  pub(crate) fn from_landmass(
475    animation_link: &landmass::ReachedAnimationLink<CS>,
476    link_id_to_entity: &HashMap<AnimationLinkId, Entity>,
477  ) -> Self {
478    Self {
479      start_point: animation_link.start_point.clone(),
480      end_point: animation_link.end_point.clone(),
481      link_entity: *link_id_to_entity.get(&animation_link.link_id).unwrap(),
482    }
483  }
484}
485
486/// Updates the [`ReachedAnimationLink`] component (including adding and
487/// removing it) on an agent to match the state in `landmass`.
488pub(crate) fn sync_agent_reached_animation_link<CS: CoordinateSystem>(
489  mut agents: Query<
490    (Entity, &ArchipelagoRef<CS>, Option<&mut ReachedAnimationLink<CS>>),
491    With<Agent<CS>>,
492  >,
493  archipelagos: Query<&Archipelago<CS>>,
494  mut commands: Commands,
495) {
496  for (agent_entity, archipelago_ref, reached_animation_link) in
497    agents.iter_mut()
498  {
499    let Ok(archipelago) = archipelagos.get(archipelago_ref.entity) else {
500      continue;
501    };
502    let Some(agent) = archipelago.get_agent(agent_entity) else {
503      continue;
504    };
505
506    let new_reached_animation_link = match agent.reached_animation_link() {
507      None => {
508        if reached_animation_link.is_some() {
509          commands.entity(agent_entity).remove::<ReachedAnimationLink<CS>>();
510        }
511        continue;
512      }
513      Some(new_reached_animation_link) => new_reached_animation_link,
514    };
515    let new_reached_animation_link = ReachedAnimationLink::from_landmass(
516      new_reached_animation_link,
517      &archipelago.reverse_animation_links,
518    );
519    match reached_animation_link {
520      None => {
521        commands.entity(agent_entity).insert(new_reached_animation_link);
522      }
523      Some(mut reached_animation_link) => {
524        *reached_animation_link = new_reached_animation_link;
525      }
526    }
527  }
528}