use bevy::prelude::*;
use crate::model::{FIRE_KELVIN, blackbody_colour, fire_phase, flame_light_offset, flame_mid};
use crate::{
FireKind, FlameEmbers, FlamePlugin, FlameSite, FlameSmoke, FlameTongues, FuelBed, LightPool,
LightsSystems, PoolCandidate,
};
pub const FIRE_EDGE_LUX: f32 = 250.0;
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[reflect(Component, Default)]
pub struct Fire {
pub kind: FireKind,
pub lumens: Option<f32>,
pub phase: Option<f32>,
pub color: Option<Color>,
pub embers: bool,
pub smoke: bool,
pub tongues: bool,
pub bed: bool,
}
impl Default for Fire {
fn default() -> Self {
Self::new(FireKind::Torch)
}
}
impl Fire {
pub const fn new(kind: FireKind) -> Self {
Self {
kind,
lumens: None,
phase: None,
color: None,
embers: true,
smoke: true,
tongues: true,
bed: true,
}
}
pub fn lit(kind: FireKind, lux: f32, over: f32) -> Self {
Self { lumens: Some(kind.lumens(lux, over)), ..Self::new(kind) }
}
pub fn problems(&self) -> Vec<crate::Problem> {
use crate::model::problem::{finite, non_negative, positive};
let mut out = Vec::new();
if let Some(lumens) = self.lumens {
positive(&mut out, "lumens", lumens);
}
if let Some(phase) = self.phase {
finite(&mut out, "phase", phase);
}
if let Some(color) = self.color {
let color = LinearRgba::from(color);
non_negative(&mut out, "color.red", color.red);
non_negative(&mut out, "color.green", color.green);
non_negative(&mut out, "color.blue", color.blue);
}
out
}
fn parts(&self, entity: Entity) -> Parts {
let kind = self.kind;
let phase = self.phase.unwrap_or_else(|| fire_phase(entity.to_bits()));
let lumens =
self.lumens.unwrap_or_else(|| kind.lumens(FIRE_EDGE_LUX, flame_light_offset(kind.height())[1]));
let flame_colour = self.color.map(|color| {
let light = LinearRgba::from(color);
let [red, green, blue] = flame_mid([light.red, light.green, light.blue]);
Color::from(LinearRgba::new(red, green, blue, light.alpha))
});
let light_colour = self.color.unwrap_or_else(|| {
let [red, green, blue] = blackbody_colour(FIRE_KELVIN, 1.0);
Color::from(LinearRgba::rgb(red, green, blue))
});
Parts {
site: FlameSite { phase, fire: Some(entity), color: flame_colour, ..kind.site() },
candidate: PoolCandidate {
phase: Some(phase),
color: Some(light_colour),
..kind.candidate_at_mean(lumens)
},
embers: if self.embers { kind.embers() } else { None },
smoke: if self.smoke { kind.smoke() } else { None },
tongues: if self.tongues { kind.tongues() } else { None },
bed: if self.bed { kind.bed() } else { None },
}
}
}
struct Parts {
site: FlameSite,
candidate: PoolCandidate,
embers: Option<FlameEmbers>,
smoke: Option<FlameSmoke>,
tongues: Option<FlameTongues>,
bed: Option<FuelBed>,
}
pub struct FirePlugin;
impl Plugin for FirePlugin {
fn build(&self, app: &mut App) {
crate::chain_sets(app);
app.register_type::<Fire>().add_systems(Update, expand_fires.before(LightsSystems::Pool));
crate::report::watch::<Fire>(app);
}
fn finish(&self, app: &mut App) {
if !app.is_plugin_added::<FlamePlugin>() {
error!("bevy_lights: FirePlugin needs FlamePlugin; a Fire will have a light and no flame drawn");
}
if !app.is_plugin_added::<crate::FlickerPlugin>() {
error!("bevy_lights: FirePlugin needs FlickerPlugin; a Fire's light will not breathe");
}
if !app.world().contains_resource::<LightPool>() {
error!("bevy_lights: FirePlugin needs LightPoolPlugin; a Fire will have a flame and no light");
}
}
}
fn expand_fires(mut commands: Commands, fires: Query<(Entity, Ref<Fire>), Changed<Fire>>) {
for (entity, fire) in &fires {
let Ok(mut entity) = commands.get_entity(entity) else { continue };
let parts = fire.parts(entity.id());
if fire.is_added() {
entity.insert_if_new((parts.site, parts.candidate));
if let Some(embers) = parts.embers {
entity.insert_if_new(embers);
}
if let Some(smoke) = parts.smoke {
entity.insert_if_new(smoke);
}
if let Some(tongues) = parts.tongues {
entity.insert_if_new(tongues);
}
if let Some(bed) = parts.bed {
entity.insert_if_new(bed);
}
} else {
entity.insert((parts.site, parts.candidate));
match parts.embers {
Some(embers) => entity.insert(embers),
None => entity.remove::<FlameEmbers>(),
};
match parts.smoke {
Some(smoke) => entity.insert(smoke),
None => entity.remove::<FlameSmoke>(),
};
match parts.tongues {
Some(tongues) => entity.insert(tongues),
None => entity.remove::<FlameTongues>(),
};
match parts.bed {
Some(bed) => entity.insert(bed),
None => entity.remove::<FuelBed>(),
};
}
}
}