use bevy::color::Color;
use bevy::ecs::prelude::*;
use bevy::mesh::{Mesh3d, MeshTag};
use bevy::reflect::Reflect;
pub(crate) const WITHERED_LEAF: Color = Color::srgb(0.47, 0.31, 0.11);
pub(crate) const TAG_SHIFT: u32 = 8;
pub(crate) const TAG_LOOK_MASK: u32 = (1 << TAG_SHIFT) - 1;
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
#[reflect(Component)]
pub enum TreeCondition {
#[default]
Healthy,
Sick,
Dying,
}
impl TreeCondition {
pub const fn defoliation(self) -> f32 {
match self {
Self::Healthy => 0.0,
Self::Sick => 0.35,
Self::Dying => 0.8,
}
}
pub const fn withering(self) -> f32 {
match self {
Self::Healthy => 0.0,
Self::Sick => 0.5,
Self::Dying => 1.0,
}
}
pub(crate) const fn tag_bits(self) -> u32 {
(self as u32) << TAG_SHIFT
}
}
pub(crate) fn leaf_decline_moment(phase: f32) -> f32 {
let x = phase * 4.61 + 0.719;
x - x.floor()
}
pub(crate) fn tag_for(look: u32, condition: Option<&TreeCondition>) -> u32 {
(look & TAG_LOOK_MASK) | condition.map_or(0, |condition| condition.tag_bits())
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct Decline {
lost: f32,
withering: f32,
}
impl Decline {
pub(crate) fn new(condition: Option<&TreeCondition>, hull: bool) -> Self {
let condition = condition.copied().unwrap_or_default();
Self { lost: if hull { 0.0 } else { condition.defoliation() }, withering: condition.withering() }
}
pub(crate) fn has_lost(&self, phase: f32) -> bool {
self.lost > 0.0 && leaf_decline_moment(phase) < self.lost
}
pub(crate) fn withered(&self, phase: f32) -> f32 {
if self.withering == 0.0 {
return 0.0;
}
let x = phase * 2.83 + 0.37;
(self.withering + 0.8 * ((x - x.floor()) - 0.5)).clamp(0.0, 1.0)
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn sync_condition_tags(
mut commands: Commands,
ill: Query<(&TreeCondition, &Children), Or<(Changed<TreeCondition>, Changed<Children>)>>,
mut removed: RemovedComponents<TreeCondition>,
standing: Query<&Children>,
has: Query<(), With<TreeCondition>>,
mut meshes: Query<Option<&mut MeshTag>, With<Mesh3d>>,
) {
for tree in removed.read() {
if has.contains(tree) {
continue;
}
let Ok(children) = standing.get(tree) else { continue };
for child in children.iter() {
if let Ok(Some(mut tag)) = meshes.get_mut(child)
&& tag.value & !TAG_LOOK_MASK != 0
{
tag.value &= TAG_LOOK_MASK;
}
}
}
for (condition, children) in &ill {
let bits = condition.tag_bits();
for child in children.iter() {
match meshes.get_mut(child) {
Ok(Some(mut tag)) => {
let value = (tag.value & TAG_LOOK_MASK) | bits;
if tag.value != value {
tag.value = value;
}
}
Ok(None) if bits != 0 => {
commands.entity(child).insert(MeshTag::new(bits));
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
FellTree, FloraIndex, IndexedTree, LodSettings, PoolColors, PooledLook, PooledTree, ProceduralTree, RecolorTree, TreeLod, TreePool,
TreePreset,
build_pool,
};
use crate::felling::Felled;
use crate::shading::FloraMaterial;
use bevy::app::App;
use bevy::asset::{AssetApp, AssetId, AssetPlugin, Assets};
use bevy::color::Color;
use bevy::math::Vec2;
use bevy::mesh::Mesh;
use bevy::pbr::{MeshMaterial3d, StandardMaterial};
use bevy::time::TimeUpdateStrategy;
use bevy::transform::components::Transform;
fn app() -> App {
let mut app = App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default(), bevy::transform::TransformPlugin));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(crate::FloraPlugin);
app.insert_resource(TimeUpdateStrategy::ManualDuration(std::time::Duration::from_secs_f32(1.0 / 30.0)));
app
}
fn pool(app: &mut App, preset: TreePreset) -> bevy::asset::Handle<TreePool> {
let built = build_pool(&preset.params(), 2, f32::INFINITY, &LodSettings::default(), &mut app.world_mut().resource_mut::<Assets<Mesh>>());
app.world_mut().resource_mut::<Assets<TreePool>>().add(built)
}
fn condition_of(app: &App, tree: Entity) -> Option<TreeCondition> {
app.world().resource::<FloraIndex>().condition_of(tree)
}
fn settle(app: &mut App) {
app.update();
app.update();
}
#[test]
fn a_condition_set_on_a_tree_reads_back_through_the_index_as_set_changed_and_cleared() {
let mut app = app();
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), Transform::from_xyz(10.0, 0.0, 0.0))).id();
let birch = app.world_mut().spawn((ProceduralTree(TreePreset::Birch.params()), Transform::from_xyz(-10.0, 0.0, 0.0))).id();
let handle = pool(&mut app, TreePreset::Pine);
let pine = app.world_mut().spawn((PooledTree { pool: handle, variant: 1 }, Transform::from_xyz(0.0, 0.0, 20.0))).id();
settle(&mut app);
for tree in [oak, birch, pine] {
assert_eq!(condition_of(&app, tree), Some(TreeCondition::Healthy), "a tree with no condition is healthy");
}
let before = *app.world().resource::<FloraIndex>().get(oak).unwrap();
app.world_mut().entity_mut(oak).insert(TreeCondition::Sick);
app.world_mut().entity_mut(pine).insert(TreeCondition::Dying);
settle(&mut app);
assert_eq!(condition_of(&app, oak), Some(TreeCondition::Sick));
assert_eq!(condition_of(&app, pine), Some(TreeCondition::Dying));
assert_eq!(condition_of(&app, birch), Some(TreeCondition::Healthy));
let index = app.world().resource::<FloraIndex>();
assert_eq!(index.get(pine).unwrap().condition, TreeCondition::Dying);
assert_eq!(IndexedTree { condition: TreeCondition::Healthy, ..*index.get(oak).unwrap() }, before);
assert_eq!(index.nearest(Vec2::new(0.5, 20.0), 2.0).map(|t| (t.entity, t.condition)), Some((pine, TreeCondition::Dying)));
let ill: Vec<Entity> = index.iter().filter(|t| t.condition >= TreeCondition::Sick).map(|t| t.entity).collect();
assert_eq!(ill.len(), 2);
assert!(ill.contains(&oak) && ill.contains(&pine));
let near_the_oak: Vec<_> = index.within(Vec2::new(10.0, 0.0), 3.0).map(|t| (t.entity, t.condition)).collect();
assert_eq!(near_the_oak, vec![(oak, TreeCondition::Sick)]);
app.world_mut().get_mut::<Transform>(oak).unwrap().translation.x = 12.0;
app.world_mut().get_mut::<ProceduralTree>(oak).unwrap().0.seed = 9;
settle(&mut app);
let moved = *app.world().resource::<FloraIndex>().get(oak).unwrap();
assert!((moved.base.x - 12.0).abs() < 1e-3, "the oak stands at x = {}", moved.base.x);
assert_eq!(moved.condition, TreeCondition::Sick, "a moved and rebuilt tree");
app.world_mut().entity_mut(oak).insert(TreeCondition::Dying);
app.world_mut().entity_mut(pine).insert(TreeCondition::Healthy);
settle(&mut app);
assert_eq!(condition_of(&app, oak), Some(TreeCondition::Dying));
assert_eq!(condition_of(&app, pine), Some(TreeCondition::Healthy));
app.world_mut().entity_mut(oak).remove::<TreeCondition>();
settle(&mut app);
assert_eq!(condition_of(&app, oak), Some(TreeCondition::Healthy), "no component, a healthy tree");
app.world_mut().entity_mut(oak).insert(TreeCondition::Sick);
settle(&mut app);
app.world_mut().entity_mut(oak).remove::<TreeCondition>();
app.world_mut().entity_mut(oak).insert(TreeCondition::Dying);
settle(&mut app);
assert_eq!(condition_of(&app, oak), Some(TreeCondition::Dying));
app.world_mut().despawn(oak);
settle(&mut app);
assert_eq!(condition_of(&app, oak), None);
}
#[test]
fn a_pooled_trees_condition_is_its_own_and_survives_what_the_pool_does() {
let mut app = app();
let handle = pool(&mut app, TreePreset::Pine);
let spawn = |app: &mut App, x: f32| {
app.world_mut().spawn((PooledTree { pool: handle.clone(), variant: 0 }, Transform::from_xyz(x, 0.0, 0.0))).id()
};
let (marked, neighbour) = (spawn(&mut app, 0.0), spawn(&mut app, 20.0));
app.world_mut().entity_mut(marked).insert(TreeCondition::Dying);
settle(&mut app);
assert_eq!(condition_of(&app, marked), Some(TreeCondition::Dying));
assert_eq!(condition_of(&app, neighbour), Some(TreeCondition::Healthy));
app.world_mut().get_mut::<PooledTree>(marked).unwrap().variant = 1;
settle(&mut app);
assert_eq!(condition_of(&app, marked), Some(TreeCondition::Dying), "after a change of variant");
let _ = app.world_mut().resource_mut::<Assets<TreePool>>().get_mut(&handle);
settle(&mut app);
assert_eq!(condition_of(&app, marked), Some(TreeCondition::Dying), "after the pool was modified");
assert_eq!(condition_of(&app, neighbour), Some(TreeCondition::Healthy));
let pooled = app.world_mut().entity_mut(marked).take::<PooledTree>().unwrap();
settle(&mut app);
assert_eq!(condition_of(&app, marked), None);
assert_eq!(app.world().get::<TreeCondition>(marked), Some(&TreeCondition::Dying));
app.world_mut().entity_mut(marked).insert(pooled);
settle(&mut app);
assert_eq!(condition_of(&app, marked), Some(TreeCondition::Dying), "after it was put back");
}
#[test]
fn a_condition_set_before_the_tree_is_indexed_applies_when_it_is() {
let mut app = app();
let handle = app.world().resource::<Assets<TreePool>>().reserve_handle();
let tree = app
.world_mut()
.spawn((PooledTree { pool: handle.clone(), variant: 0 }, TreeCondition::Sick, Transform::from_xyz(4.0, 0.0, 4.0)))
.id();
settle(&mut app);
assert_eq!(condition_of(&app, tree), None, "no pool, no trunk to index");
app.world_mut().entity_mut(tree).insert(TreeCondition::Dying);
settle(&mut app);
assert_eq!(condition_of(&app, tree), None);
let built = build_pool(&TreePreset::Pine.params(), 1, f32::INFINITY, &LodSettings::default(), &mut app.world_mut().resource_mut::<Assets<Mesh>>());
app.world_mut().resource_mut::<Assets<TreePool>>().insert(handle.id(), built).unwrap();
settle(&mut app);
assert_eq!(condition_of(&app, tree), Some(TreeCondition::Dying), "it shows what was last set");
}
#[test]
fn a_felled_trees_condition_does_not_panic_and_stays_on_its_entity() {
let mut app = app();
let handle = pool(&mut app, TreePreset::Pine);
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeCondition::Sick, Transform::default())).id();
let pine = app
.world_mut()
.spawn((PooledTree { pool: handle, variant: 0 }, TreeCondition::Dying, Transform::from_xyz(30.0, 0.0, 0.0)))
.id();
settle(&mut app);
assert_eq!((condition_of(&app, oak), condition_of(&app, pine)), (Some(TreeCondition::Sick), Some(TreeCondition::Dying)));
for tree in [oak, pine] {
app.world_mut().entity_mut(tree).insert(FellTree { speed: 6.0, ..Default::default() });
}
for frame in 0..400 {
if frame == 3 {
app.world_mut().entity_mut(oak).insert(TreeCondition::Dying);
}
app.update();
}
for tree in [oak, pine] {
assert!(app.world().get::<Felled>(tree).is_some_and(|f| f.at_rest), "the tree did not come to rest");
assert_eq!(condition_of(&app, tree), None, "a felled tree is not in the index");
}
assert_eq!(app.world().get::<TreeCondition>(oak), Some(&TreeCondition::Dying));
assert_eq!(app.world().get::<TreeCondition>(pine), Some(&TreeCondition::Dying));
app.world_mut().entity_mut(oak).insert(TreeCondition::Healthy);
app.world_mut().entity_mut(pine).remove::<TreeCondition>();
settle(&mut app);
assert_eq!(app.world().get::<TreeCondition>(oak), Some(&TreeCondition::Healthy));
assert!(app.world().get::<TreeCondition>(pine).is_none());
assert_eq!(app.world().resource::<FloraIndex>().len(), 0);
}
#[test]
fn a_recolour_and_a_condition_in_one_frame_both_apply_and_equal_looks_still_share_a_material() {
const BLUE: PoolColors =
PoolColors { wood: Color::srgb(0.10, 0.20, 0.60), leaves: Color::srgb(0.30, 0.55, 0.95), unlit: false };
let run = |condition: bool| {
let mut app = app();
app.insert_resource(crate::FloraMaterialSupport);
let trees: Vec<Entity> = (0..2)
.map(|i| {
let at = Transform::from_xyz(15.0 * i as f32, 0.0, 0.0);
app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeLod::default(), at)).id()
})
.collect();
settle(&mut app);
for &tree in &trees {
app.world_mut().write_message(RecolorTree { tree, colors: BLUE });
}
if condition {
app.world_mut().entity_mut(trees[0]).insert(TreeCondition::Dying);
}
settle(&mut app);
(app, trees)
};
let materials = |app: &App, tree: Entity| -> Vec<(AssetId<FloraMaterial>, Color)> {
let children = app.world().get::<bevy::ecs::hierarchy::Children>(tree).expect("a built tree has children");
let assets = app.world().resource::<Assets<FloraMaterial>>();
let mut found = Vec::new();
for child in children.iter() {
if let Some(handle) = app.world().get::<MeshMaterial3d<FloraMaterial>>(child) {
let material = assets.get(&handle.0).expect("a live material");
let wanted = if material.extension.leaf { BLUE.leaves } else { BLUE.wood };
found.push((handle.0.id(), wanted));
assert_eq!(material.base.base_color, wanted, "the recolour applied (leaf: {})", material.extension.leaf);
}
}
found
};
let (control, _) = run(false);
let (app, trees) = run(true);
let (marked, other) = (materials(&app, trees[0]), materials(&app, trees[1]));
assert!(marked.len() >= 6, "every level of detail of the marked tree is recoloured");
assert_eq!(condition_of(&app, trees[0]), Some(TreeCondition::Dying));
assert_eq!(condition_of(&app, trees[1]), Some(TreeCondition::Healthy));
assert_eq!(marked, other, "trees of equal colours share their materials, marked or not");
let live = |app: &App| app.world().resource::<Assets<FloraMaterial>>().len();
assert_eq!(live(&app), live(&control), "a condition made no material of its own");
}
fn tags(app: &App, tree: Entity) -> Vec<u32> {
let world = app.world();
let Some(children) = world.get::<Children>(tree) else { return Vec::new() };
children
.iter()
.filter(|&child| world.get::<Mesh3d>(child).is_some())
.map(|child| world.get::<MeshTag>(child).map_or(0, |tag| tag.value))
.collect()
}
#[test]
fn the_meshes_of_a_tree_carry_its_condition_as_set_changed_rebuilt_and_cleared() {
let mut app = app();
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeLod::default())).id();
let other = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), Transform::from_xyz(20.0, 0.0, 0.0))).id();
settle(&mut app);
let all = |app: &App, tree: Entity, bits: u32| {
let found = tags(app, tree);
!found.is_empty() && found.iter().all(|&tag| tag == bits)
};
assert!(tags(&app, oak).len() >= 6, "wood and leaves at every level of detail");
assert!(all(&app, oak, 0) && all(&app, other, 0), "a tree with no condition has untagged meshes");
app.world_mut().entity_mut(oak).insert(TreeCondition::Sick);
settle(&mut app);
assert!(all(&app, oak, TreeCondition::Sick.tag_bits()));
assert!(all(&app, other, 0), "no other tree is touched");
app.world_mut().entity_mut(oak).insert(TreeCondition::Dying);
settle(&mut app);
assert!(all(&app, oak, TreeCondition::Dying.tag_bits()));
app.world_mut().get_mut::<ProceduralTree>(oak).unwrap().0.seed = 9;
settle(&mut app);
assert!(all(&app, oak, TreeCondition::Dying.tag_bits()), "{:?}", tags(&app, oak));
app.world_mut().entity_mut(oak).insert(TreeCondition::Healthy);
settle(&mut app);
assert!(all(&app, oak, 0));
app.world_mut().entity_mut(oak).insert(TreeCondition::Dying);
settle(&mut app);
app.world_mut().entity_mut(oak).remove::<TreeCondition>();
settle(&mut app);
assert!(all(&app, oak, 0));
app.world_mut().entity_mut(oak).insert(TreeCondition::Sick);
settle(&mut app);
app.world_mut().entity_mut(oak).remove::<TreeCondition>().insert(TreeCondition::Dying);
settle(&mut app);
assert!(all(&app, oak, TreeCondition::Dying.tag_bits()));
}
#[test]
fn a_pooled_trees_meshes_keep_their_looks_tag_beside_the_condition() {
const RED: PoolColors = PoolColors { wood: Color::srgb(0.3, 0.1, 0.1), leaves: Color::srgb(0.8, 0.1, 0.1), unlit: false };
let mut app = app();
let mut built = build_pool(&TreePreset::Pine.params(), 2, f32::INFINITY, &LodSettings::default(), &mut app.world_mut().resource_mut::<Assets<Mesh>>());
built.add_look(RED);
let handle = app.world_mut().resource_mut::<Assets<TreePool>>().add(built);
let (a, b) = (PooledTree { pool: handle.clone(), variant: 0 }, PooledTree { pool: handle, variant: 1 });
let sick = app.world_mut().spawn((a, TreeCondition::Sick)).id();
let well = app.world_mut().spawn((b, Transform::from_xyz(9.0, 0.0, 0.0))).id();
settle(&mut app);
let (look, bits) = (|tag: u32| tag & TAG_LOOK_MASK, |tag: u32| tag & !TAG_LOOK_MASK);
let first = tags(&app, sick);
assert!(first.len() >= 6 && first.iter().all(|&tag| bits(tag) == TreeCondition::Sick.tag_bits()), "{first:?}");
let glide = look(first[0]);
assert!(glide != 0 && first.iter().all(|&tag| look(tag) == glide), "the look's tag is still there: {first:?}");
assert!(tags(&app, well).iter().all(|&tag| bits(tag) == 0));
app.world_mut().entity_mut(sick).insert(PooledLook(1));
settle(&mut app);
let moved = tags(&app, sick);
let glide_now = look(moved[0]);
assert!(glide_now != 0 && glide_now != glide, "the other look's tag: {moved:?}");
assert!(moved.iter().all(|&tag| look(tag) == glide_now && bits(tag) == TreeCondition::Sick.tag_bits()), "{moved:?}");
app.world_mut().entity_mut(sick).insert(TreeCondition::Dying);
settle(&mut app);
assert!(tags(&app, sick).iter().all(|&tag| look(tag) == glide_now && bits(tag) == TreeCondition::Dying.tag_bits()));
app.world_mut().entity_mut(sick).remove::<TreeCondition>();
settle(&mut app);
assert!(tags(&app, sick).iter().all(|&tag| tag == glide_now), "{:?}", tags(&app, sick));
}
#[test]
fn a_declined_leaf_is_lost_by_its_own_draw_and_withers_by_its_class() {
let healthy = Decline::new(None, false);
let sick = Decline::new(Some(&TreeCondition::Sick), false);
let dying = Decline::new(Some(&TreeCondition::Dying), false);
let hull = Decline::new(Some(&TreeCondition::Dying), true);
let phases: Vec<f32> = (0..4000).map(|k| k as f32 * 0.00157).collect();
let lost = |decline: &Decline| phases.iter().filter(|&&p| decline.has_lost(p)).count() as f32 / phases.len() as f32;
assert_eq!(lost(&healthy), 0.0);
assert!((lost(&sick) - TreeCondition::Sick.defoliation()).abs() < 0.03, "{}", lost(&sick));
assert!((lost(&dying) - TreeCondition::Dying.defoliation()).abs() < 0.03, "{}", lost(&dying));
assert_eq!(lost(&hull), 0.0, "a hull crown keeps its facets");
assert!(phases.iter().all(|&p| !sick.has_lost(p) || dying.has_lost(p)));
assert!(phases.iter().all(|&p| healthy.withered(p) == 0.0));
let mean = |decline: &Decline| phases.iter().map(|&p| decline.withered(p)).sum::<f32>() / phases.len() as f32;
assert!((mean(&sick) - TreeCondition::Sick.withering()).abs() < 0.03, "{}", mean(&sick));
assert!(mean(&dying) > 0.85 && mean(&dying) <= 1.0, "{}", mean(&dying));
assert!(phases.iter().all(|&p| (0.0..=1.0).contains(&sick.withered(p)) && (0.0..=1.0).contains(&dying.withered(p))));
let spread = phases.iter().map(|&p| sick.withered(p)).fold((1.0f32, 0.0f32), |(lo, hi), t| (lo.min(t), hi.max(t)));
assert!(spread.1 - spread.0 > 0.6, "{spread:?}");
}
}