use std::marker::PhantomData;
use bevy::light::PointLight;
use bevy::prelude::*;
use crate::flicker::{Flicker, FlickerPlugin};
use crate::{LightsSystems, chain_sets};
pub struct LightPoolPlugin<F: Component> {
_focus: PhantomData<fn() -> F>,
}
impl<F: Component> Default for LightPoolPlugin<F> {
fn default() -> Self {
Self { _focus: PhantomData }
}
}
impl<F: Component> LightPoolPlugin<F> {
pub fn new() -> Self {
Self::default()
}
}
impl<F: Component> Plugin for LightPoolPlugin<F> {
fn build(&self, app: &mut App) {
chain_sets(app);
app.init_resource::<LightPool>()
.register_type::<LightPool>()
.register_type::<PoolCandidate>()
.register_type::<PoolLight>()
.add_systems(Update, assign_pool::<F>.in_set(LightsSystems::Pool));
}
fn finish(&self, app: &mut App) {
if !app.is_plugin_added::<FlickerPlugin>() {
error!("bevy_lights: LightPoolPlugin needs FlickerPlugin; pool lights will stay dark");
}
}
}
#[derive(Resource, Clone, Debug, Reflect)]
#[reflect(Resource)]
pub struct LightPool {
pub size: usize,
pub reach: f32,
pub gain: f32,
pub light: PointLight,
}
impl Default for LightPool {
fn default() -> Self {
Self {
size: 16,
reach: 60.0,
gain: 1.0,
light: PointLight { intensity: 0.0, range: 10.0, shadow_maps_enabled: false, ..default() },
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[reflect(Component)]
pub struct PoolCandidate {
pub offset: Vec3,
pub intensity: f32,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[reflect(Component)]
pub struct PoolLight {
pub slot: usize,
}
#[derive(Clone, Copy, PartialEq)]
struct SlotLook {
color: Color,
range: f32,
radius: f32,
shadow_maps_enabled: bool,
}
impl SlotLook {
fn of(light: &PointLight) -> Self {
Self {
color: light.color,
range: light.range,
radius: light.radius,
shadow_maps_enabled: light.shadow_maps_enabled,
}
}
fn apply(self, light: &mut PointLight) {
if light.color != self.color
|| light.range != self.range
|| light.radius != self.radius
|| light.shadow_maps_enabled != self.shadow_maps_enabled
{
light.color = self.color;
light.range = self.range;
light.radius = self.radius;
light.shadow_maps_enabled = self.shadow_maps_enabled;
}
}
}
type Assignment = (f32, Entity, Vec3, f32);
fn assign_pool<F: Component>(
mut commands: Commands,
pool: Res<LightPool>,
focus: Query<&GlobalTransform, With<F>>,
candidates: Query<(Entity, &GlobalTransform, Option<&Visibility>, &PoolCandidate), Without<PoolLight>>,
mut slots: Query<(Entity, &PoolLight, &mut Transform, &mut Visibility, &mut Flicker, &mut PointLight)>,
mut applied: Local<Option<SlotLook>>,
) {
let existing: Vec<(Entity, usize)> =
slots.iter().map(|(entity, light, ..)| (entity, light.slot)).collect();
for slot in 0..pool.size {
if !existing.iter().any(|(_, at)| *at == slot) {
commands.spawn((
Name::new(format!("Pool light {slot}")),
PoolLight { slot },
PointLight { intensity: 0.0, ..pool.light },
Flicker { base: 0.0, phase: 1.3 * slot as f32 },
Transform::default(),
Visibility::Hidden,
));
}
}
for (entity, slot) in &existing {
if *slot >= pool.size {
commands.entity(*entity).despawn();
}
}
let look = SlotLook::of(&pool.light);
if *applied != Some(look) {
for (_, _, _, _, _, mut light) in &mut slots {
look.apply(&mut light);
}
*applied = Some(look);
}
let focus_at = {
let mut found = focus.iter();
match (found.next(), found.next()) {
(Some(at), None) => Some(at.translation()),
(Some(_), Some(_)) => {
let count = 2 + found.count();
warn_once!(
"bevy_lights: LightPoolPlugin found {count} focus entities; the pool needs exactly one"
);
None
}
_ => None,
}
};
let mut lit: Vec<Assignment> = match focus_at {
Some(focus_at) if pool.gain > 0.0 => candidates
.iter()
.filter(|(_, _, visibility, _)| !visibility.is_some_and(|v| *v == Visibility::Hidden))
.map(|(entity, at, _, candidate)| {
let position = at.translation();
(
position.distance_squared(focus_at),
entity,
position + candidate.offset,
candidate.intensity,
)
})
.filter(|(distance_squared, ..)| *distance_squared <= pool.reach * pool.reach)
.collect(),
_ => Vec::new(),
};
lit.sort_by(|a, b| a.0.total_cmp(&b.0).then_with(|| a.1.to_bits().cmp(&b.1.to_bits())));
for (_, slot, mut transform, mut visibility, mut flicker, _) in &mut slots {
match lit.get(slot.slot) {
Some(&(_, _, at, intensity)) => {
transform.translation = at;
flicker.base = intensity * pool.gain;
visibility.set_if_neq(Visibility::Inherited);
}
None => {
flicker.base = 0.0;
visibility.set_if_neq(Visibility::Hidden);
}
}
}
}