1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
//! Plugin for adding a progress bar systems to the game.
use bevy::prelude::*;
use bevy::sprite::MaterialMesh2dBundle;
use crate::progress_bar::{Percentage, ProgressBarConfig};
use super::BarState;
/// Plugin for adding a progress bar systems to the game.
///
/// This plugin adds a system to draw progress bars for entities with a `Percentage` component.
///
/// To have the progress bars be drawn, add the `ProgressBarConfig` component to the entity to
/// configure how to draw the progress bar on that entity for the specific `Percentage` component.
#[allow(clippy::module_name_repetitions)]
pub struct ProgressBarPlugin<T: Percentage + Component> {
_marker: std::marker::PhantomData<T>,
}
impl<T: Percentage + Component> Default for ProgressBarPlugin<T> {
fn default() -> Self {
Self {
_marker: std::marker::PhantomData,
}
}
}
impl<T: Percentage + Component> Plugin for ProgressBarPlugin<T> {
fn build(&self, app: &mut App) {
// systems for:
// - spawn
// - update
// - remove
app.add_systems(Update, (spawn_progress_bars::<T>, remove::<T>, update::<T>));
}
}
/// Component to store the progress bar entities.
///
/// This component is added to the entity with the `Percentage` component.
///
/// The tuple contains the background and foreground entities (in that order).
#[derive(Component, Reflect)]
struct WithProgressBar<T: Percentage + Component> {
/// The background bar entity
pub foreground: Entity,
/// The foreground bar entity
pub background: Entity,
/// Handle for the foreground bar material in the ok state
pub ok_handle: Handle<ColorMaterial>,
/// Handle for the foreground bar material in the moderate state
pub moderate_handle: Handle<ColorMaterial>,
/// Handle for the foreground bar material in the critical state
pub critical_handle: Handle<ColorMaterial>,
/// Marker for the generic type
#[reflect(ignore)]
_marker: std::marker::PhantomData<T>,
}
impl<T: Percentage + Component> WithProgressBar<T> {
const fn get(&self) -> (Entity, Entity) {
(self.background, self.foreground)
}
fn get_material(&self, config: &ProgressBarConfig<T>, percentage: &T) -> Handle<ColorMaterial> {
match config.get_state(percentage) {
BarState::Ok => self.ok_handle.clone(),
BarState::Moderate => self.moderate_handle.clone(),
BarState::Critical => self.critical_handle.clone(),
}
}
}
/// System to spawn progress bars for entities with a `Percentage` component.
///
/// The `Added<T>` query just gets entities which are new and have the `T` component.
#[allow(clippy::needless_pass_by_value)]
fn spawn_progress_bars<T: Percentage + Component>(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
query: Query<(Entity, &T, &ProgressBarConfig<T>), Added<T>>,
transform_query: Query<&Transform>,
) {
for (entity, percentage, config) in query.iter() {
// spawn progress bar
let foreground_color = materials.add(config.color(percentage).into());
let foreground_color_moderate =
materials.add(config.color_for_state(&BarState::Moderate).into());
let foreground_color_critical =
materials.add(config.color_for_state(&BarState::Critical).into());
let background_color = materials.add(config.background_color().into());
let Ok(entity_transform) = transform_query.get(entity) else {
tracing::warn!("Failed to get parent transform for progress bar");
continue;
};
let bar_background = commands
.spawn(MaterialMesh2dBundle {
mesh: meshes.add(config.background_mesh()).into(),
material: background_color,
transform: config.background_transform(entity_transform),
..default()
})
.id();
let bar_foreground = commands
.spawn(MaterialMesh2dBundle {
mesh: meshes.add(config.foreground_mesh(percentage)).into(),
material: foreground_color.clone(),
transform: config.foreground_transform(entity_transform, percentage),
..default()
})
.id();
commands
.entity(entity)
.insert(WithProgressBar::<T> {
foreground: bar_foreground,
background: bar_background,
ok_handle: foreground_color,
moderate_handle: foreground_color_moderate,
critical_handle: foreground_color_critical,
_marker: std::marker::PhantomData,
})
.add_child(bar_background)
.add_child(bar_foreground);
}
}
/// System to update progress bars for entities with a `Percentage` component.
/// This system is run on every frame.
/// The `Changed<T>` query gets entities which have the `T` component and have changed.
///
/// This system updates the progress bar's foreground mesh to match the percentage.
///
/// # Parameters
///
/// * `meshes`: The mesh asset storage. This is used because we update a mesh with a new one for the foreground.
/// Todo: This probably is terribly inefficient, since if there's a lot of progress bars.. potentially a lot of meshes are being created.
/// * `parent_query`: The query for the parent entity, the entity with the `Percentage` component. We also grab the
/// `ProgressBarConfig` component to get the configuration for the progress bar, and the `WithProgressBar` component
/// to get the progress bar entities. Finally, we grab the children of the parent entity because we spawn the progress
/// bar entities as children of the parent entity.
/// * `mesh_query`: The query for the progress bar entities. We grab the mesh and transform components to update the mesh and transform.
#[allow(clippy::needless_pass_by_value, clippy::type_complexity)]
fn update<T: Percentage + Component>(
mut commands: Commands,
parent_query: Query<(&T, &ProgressBarConfig<T>, &WithProgressBar<T>, &Children), Changed<T>>,
mut mesh_query: Query<&mut Transform>,
) {
parent_query.for_each(|(percentage, config, progress_bar, children)| {
let (_, foreground) = progress_bar.get();
// loop over child entity ids
for child in children {
// if this isn't the foreground entity, skip it
if *child != foreground {
continue;
}
tracing::info!(
"Updating progress bar for entity with percentage {}",
percentage.percentage()
);
// grab the mutable mesh
let Ok(mut transform) = mesh_query.get_mut(*child) else {
tracing::warn!(
"Failed to get foreground mesh for progress bar Entity {foreground:?}"
);
return;
};
// update the mesh
commands
.entity(*child)
.insert(progress_bar.get_material(config, percentage));
// update the transform
transform.scale = Vec3::new(percentage.percentage(), 1.0, 1.0);
// update the translation.x to be the percentage of the parent's width
transform.translation.x =
config.position_translation.x + (percentage.percentage() * config.size.x / 2.0);
}
});
}
#[allow(clippy::needless_pass_by_value)]
fn remove<T: Percentage + Component>(
mut commands: Commands,
mut removals: RemovedComponents<T>,
parent_query: Query<&WithProgressBar<T>>,
) {
removals.read().for_each(|entity| {
let Ok(&WithProgressBar {
foreground,
background,
..
}) = parent_query.get(entity)
else {
return;
};
commands.entity(foreground).despawn_recursive();
commands.entity(background).despawn_recursive();
});
}