use bevy::camera::visibility::VisibilitySystems;
use bevy::light::{PointLight, SpotLight};
use bevy::prelude::*;
use bevy::time::Time;
use crate::LightsSystems;
use crate::chain_sets;
use crate::model::{
FLICKER_DEPTH, KINDLE_SECONDS, SNUFF_SECONDS, flicker_depth, flicker_mean, flicker_swing, kindle_gain,
snuff_gain,
};
pub struct FlickerPlugin;
impl Plugin for FlickerPlugin {
fn build(&self, app: &mut App) {
chain_sets(app);
app.add_message::<Kindled>()
.add_message::<Snuffed>()
.register_type::<Flicker>()
.register_type::<Kindle>()
.register_type::<Snuff>()
.register_type::<crate::FireKind>()
.add_systems(Update, flicker_lights.in_set(LightsSystems::Flicker))
.add_systems(
PostUpdate,
write_lights_coming_into_view
.after(VisibilitySystems::CheckVisibility)
.after(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
);
crate::report::watch::<Flicker>(app);
crate::report::watch::<Snuff>(app);
crate::report::watch::<Kindle>(app);
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[reflect(Component)]
pub struct Flicker {
pub base: f32,
pub phase: f32,
pub diameter: Option<f32>,
pub depth: f32,
}
impl Default for Flicker {
fn default() -> Self {
Self { base: 0.0, phase: 0.0, diameter: None, depth: FLICKER_DEPTH }
}
}
impl Flicker {
pub fn at_mean(mean: f32, depth: f32) -> Self {
let depth = flicker_depth(depth);
Self { base: mean / flicker_mean(depth), depth, ..Self::default() }
}
pub fn mean(&self) -> f32 {
self.base * flicker_mean(self.depth)
}
pub fn set_mean(&mut self, mean: f32) {
self.base = mean / flicker_mean(self.depth);
}
pub fn set_depth(&mut self, depth: f32) {
let mean = self.mean();
self.depth = flicker_depth(depth);
self.set_mean(mean);
}
pub fn intensity_at(&self, t: f32, kindle: Option<f32>) -> f32 {
let gain = kindle.map_or(1.0, kindle_gain);
self.base * flicker_swing(self.diameter, self.depth, t, self.phase) * gain
}
}
impl Flicker {
pub fn problems(&self) -> Vec<crate::Problem> {
use crate::model::problem::{finite, non_negative, positive, within};
let mut out = Vec::new();
non_negative(&mut out, "base", self.base);
finite(&mut out, "phase", self.phase);
if let Some(d) = self.diameter {
positive(&mut out, "diameter", d);
}
within(&mut out, "depth", self.depth, 0.0, crate::MAX_FLICKER_DEPTH);
out
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[reflect(Component)]
pub struct Kindle {
pub elapsed: f32,
}
impl Kindle {
pub fn problems(&self) -> Vec<crate::Problem> {
let mut out = Vec::new();
crate::model::problem::non_negative(&mut out, "elapsed", self.elapsed);
out
}
}
#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Kindled {
pub entity: Entity,
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[reflect(Component)]
pub struct Snuff {
pub elapsed: f32,
}
impl Snuff {
pub fn problems(&self) -> Vec<crate::Problem> {
let mut out = Vec::new();
crate::model::problem::non_negative(&mut out, "elapsed", self.elapsed);
out
}
}
#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Snuffed {
pub entity: Entity,
}
trait Emits: Component {
fn intensity(&mut self) -> &mut f32;
}
impl Emits for PointLight {
fn intensity(&mut self) -> &mut f32 {
&mut self.intensity
}
}
impl Emits for SpotLight {
fn intensity(&mut self) -> &mut f32 {
&mut self.intensity
}
}
type PointLightFrame<'w> = (
Entity,
&'w mut PointLight,
&'w Flicker,
Option<&'w mut Kindle>,
Option<&'w mut Snuff>,
Option<&'w ViewVisibility>,
Option<Ref<'w, Visibility>>,
);
type SpotLightFrame<'w> = (
Entity,
&'w mut SpotLight,
&'w Flicker,
Option<&'w mut Kindle>,
Option<&'w mut Snuff>,
Option<&'w ViewVisibility>,
Option<Ref<'w, Visibility>>,
);
fn flicker_lights(
time: Res<Time>,
mut points: Query<PointLightFrame, Without<SpotLight>>,
mut spots: Query<SpotLightFrame, Without<PointLight>>,
cameras: Query<(), With<Camera>>,
mut kindled: MessageWriter<Kindled>,
mut snuffed: MessageWriter<Snuffed>,
) {
let t = time.elapsed_secs();
let dt = time.delta_secs();
let watched = !cameras.is_empty();
for (entity, mut light, flicker, kindle, snuff, seen, visibility) in &mut points {
let hidden = is_hidden(visibility, seen, watched);
let heard = modulate(&mut *light, *flicker, kindle, snuff, hidden, t, dt);
announce(entity, heard, &mut kindled, &mut snuffed);
}
for (entity, mut light, flicker, kindle, snuff, seen, visibility) in &mut spots {
let hidden = is_hidden(visibility, seen, watched);
let heard = modulate(&mut *light, *flicker, kindle, snuff, hidden, t, dt);
announce(entity, heard, &mut kindled, &mut snuffed);
}
}
#[derive(Clone, Copy, Default)]
struct Heard {
lit: bool,
out: bool,
}
fn announce(
entity: Entity,
heard: Heard,
kindled: &mut MessageWriter<Kindled>,
snuffed: &mut MessageWriter<Snuffed>,
) {
if heard.lit {
kindled.write(Kindled { entity });
}
if heard.out {
snuffed.write(Snuffed { entity });
}
}
fn is_hidden(visibility: Option<Ref<Visibility>>, seen: Option<&ViewVisibility>, watched: bool) -> bool {
let Some(visibility) = visibility else {
return watched && seen.is_some_and(|seen| !seen.get());
};
if *visibility == Visibility::Hidden {
return true;
}
let shown_now = visibility.is_changed() && !visibility.is_added();
watched && !shown_now && seen.is_some_and(|seen| !seen.get())
}
fn modulate<L: Emits>(
light: &mut L,
flicker: Flicker,
kindle: Option<Mut<Kindle>>,
snuff: Option<Mut<Snuff>>,
hidden: bool,
t: f32,
dt: f32,
) -> Heard {
let (elapsed, out, heard) = advance_fire(kindle, snuff, dt);
if !hidden {
*light.intensity() = level(flicker, elapsed, out, t);
}
heard
}
fn level(flicker: Flicker, elapsed: Option<f32>, out: Option<f32>, t: f32) -> f32 {
flicker.intensity_at(t, elapsed) * out.unwrap_or(1.0)
}
type PointLightSeen<'w> =
(&'w mut PointLight, &'w Flicker, Option<&'w Kindle>, Option<&'w Snuff>, &'w ViewVisibility);
type SpotLightSeen<'w> =
(&'w mut SpotLight, &'w Flicker, Option<&'w Kindle>, Option<&'w Snuff>, &'w ViewVisibility);
fn write_lights_coming_into_view(
time: Res<Time>,
mut points: Query<PointLightSeen, (Without<SpotLight>, Changed<ViewVisibility>)>,
mut spots: Query<SpotLightSeen, (Without<PointLight>, Changed<ViewVisibility>)>,
) {
let t = time.elapsed_secs();
let level_of = |flicker: &Flicker, kindle: Option<&Kindle>, snuff: Option<&Snuff>| {
level(*flicker, kindle.map(|kindle| kindle.elapsed), snuff.map(|snuff| snuff_gain(snuff.elapsed)), t)
};
for (mut light, flicker, kindle, snuff, seen) in &mut points {
if seen.get() {
light.intensity = level_of(flicker, kindle, snuff);
}
}
for (mut light, flicker, kindle, snuff, seen) in &mut spots {
if seen.get() {
light.intensity = level_of(flicker, kindle, snuff);
}
}
}
fn advance_fire(
kindle: Option<Mut<Kindle>>,
snuff: Option<Mut<Snuff>>,
dt: f32,
) -> (Option<f32>, Option<f32>, Heard) {
let mut heard = Heard::default();
let elapsed = kindle.map(|mut kindle| {
heard.lit = kindle.elapsed == 0.0;
if kindle.elapsed < KINDLE_SECONDS {
kindle.elapsed = (kindle.elapsed + dt).max(f32::MIN_POSITIVE);
}
kindle.elapsed
});
let out = snuff.map(|mut snuff| {
if snuff.elapsed < SNUFF_SECONDS {
snuff.elapsed = (snuff.elapsed + dt).min(SNUFF_SECONDS);
heard.out = snuff.elapsed >= SNUFF_SECONDS;
}
snuff_gain(snuff.elapsed)
});
(elapsed, out, heard)
}
type CandidateFire = (Entity, Option<&'static mut Kindle>, Option<&'static mut Snuff>);
type CandidateFireFilter = (With<crate::PoolCandidate>, Without<Flicker>, Or<(With<Kindle>, With<Snuff>)>);
pub(crate) fn advance_candidate_fires(
time: Res<Time>,
mut fires: Query<CandidateFire, CandidateFireFilter>,
mut kindled: MessageWriter<Kindled>,
mut snuffed: MessageWriter<Snuffed>,
) {
let dt = time.delta_secs();
for (entity, kindle, snuff) in &mut fires {
let (.., heard) = advance_fire(kindle, snuff, dt);
announce(entity, heard, &mut kindled, &mut snuffed);
}
}