use std::f32::consts::TAU;
use bevy::ecs::lifecycle::HookContext;
use bevy::ecs::world::DeferredWorld;
use bevy::prelude::*;
use bevy::ui::{AngularColorStop, BackgroundGradient, ConicGradient, Gradient, UiPosition};
use crate::model::{cooldown_sweep, cooldown_text};
use crate::skin::{GooeySkin, Role, SkinScope, skin_of};
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[component(on_insert = queue_cover_sync, on_remove = queue_cover_sync)]
pub struct Cooldown {
pub duration: f32,
pub remaining: f32,
}
impl Cooldown {
pub fn new(seconds: f32) -> Self {
Self {
duration: seconds,
remaining: seconds,
}
}
pub fn start(&mut self, seconds: f32) {
*self = Self::new(seconds);
}
pub fn is_ready(&self) -> bool {
!(self.remaining.is_finite() && self.remaining > 0.0)
}
pub fn fraction(&self) -> f32 {
if self.is_ready() || !self.duration.is_finite() || self.duration <= 0.0 {
return if self.is_ready() { 0.0 } else { 1.0 };
}
(self.remaining / self.duration).clamp(0.0, 1.0)
}
}
#[derive(EntityEvent, Clone, Debug, PartialEq, Eq, Reflect)]
#[reflect(Event, Clone, Debug)]
pub struct CooldownReady {
#[event_target]
pub entity: Entity,
}
fn queue_cover_sync(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
world
.commands()
.queue(move |world: &mut World| sync_cover(world, entity));
}
fn sync_cover(world: &mut World, entity: Entity) {
let Ok(owner) = world.get_entity(entity) else {
return;
};
let has = owner.contains::<Cooldown>();
let covers: Vec<Entity> = owner
.get::<Children>()
.into_iter()
.flat_map(|children| children.iter())
.filter(|child| world.get::<Role>(*child) == Some(&Role::Cooldown))
.collect();
match (has, covers.is_empty()) {
(true, true) => {
world.spawn((
Node {
position_type: PositionType::Absolute,
..default()
},
Role::Cooldown,
BackgroundGradient::default(),
Visibility::Hidden,
ZIndex(2),
Pickable::IGNORE,
ChildOf(entity),
children![(
Text::new(""),
TextLayout::new(Justify::Center, LineBreak::NoWrap),
Role::CooldownText,
Pickable::IGNORE,
)],
));
}
(false, false) => {
for cover in covers {
world.entity_mut(cover).despawn();
}
}
_ => {}
}
}
const EDGE_ANGLE: f32 = 0.05;
fn sweep_gradient(dim: Color, edge: Color, angle: f32) -> Gradient {
let clear = dim.with_alpha(0.0);
Gradient::Conic(ConicGradient::new(
UiPosition::CENTER,
vec![
AngularColorStop::new(clear, 0.0),
AngularColorStop::new(clear, angle),
AngularColorStop::new(edge, angle),
AngularColorStop::new(edge, angle + EDGE_ANGLE),
AngularColorStop::new(dim, angle + EDGE_ANGLE),
AngularColorStop::new(dim, TAU),
],
))
}
#[allow(clippy::too_many_arguments)]
pub fn update_cooldowns(
time: Option<Res<Time>>,
skin: Res<GooeySkin>,
scopes: Query<&SkinScope>,
parents: Query<&ChildOf>,
mut cooldowns: Query<(Entity, Mut<Cooldown>, &Children)>,
roles: Query<&Role>,
child_lists: Query<&Children>,
mut covers: Query<(&mut BackgroundGradient, &mut Visibility)>,
mut texts: Query<&mut Text>,
mut commands: Commands,
) {
let delta = time.map_or(0.0, |time| time.delta_secs());
for (entity, mut cooldown, children) in &mut cooldowns {
if cooldown.is_ready() && !cooldown.is_changed() {
continue;
}
if !cooldown.is_ready() && delta > 0.0 {
cooldown.remaining -= delta;
if cooldown.remaining <= 0.0 {
cooldown.remaining = 0.0;
commands.trigger(CooldownReady { entity });
}
}
let Some(cover) = children
.iter()
.find(|child| roles.get(*child) == Ok(&Role::Cooldown))
else {
continue;
};
let Ok((mut gradient, mut visibility)) = covers.get_mut(cover) else {
continue;
};
let running = !cooldown.is_ready();
visibility.set_if_neq(if running {
Visibility::Inherited
} else {
Visibility::Hidden
});
if running {
let look = skin_of(&skin, entity, &scopes, &parents);
let wanted = [sweep_gradient(
look.cooldown_dim(),
look.cooldown_edge(),
cooldown_sweep(cooldown.remaining, cooldown.duration),
)];
if gradient.0.as_slice() != wanted {
gradient.0 = wanted.into();
}
}
let words = cooldown_text(cooldown.remaining);
if let Ok(parts) = child_lists.get(cover) {
for part in parts {
if let Ok(mut text) = texts.get_mut(*part)
&& text.0 != words
{
text.0 = words.clone();
}
}
}
}
}