use std::f32::consts::PI;
use bevy::input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll};
use bevy::prelude::*;
use bevy_flora::{
BuckTree, FellTree, Felled, FloraPlugin, FloraSeason, GpuGrowth, Growing, ProceduralTree, TreeAge,
TreeGenerated, TreeLog, TreeNotch, TreePreset, TreeShake, TreeShakes, TreeShape, TreeStump,
};
#[derive(Resource)]
struct Showcase {
preset: usize,
seed: i32,
year: f32,
year_running: bool,
}
const YEAR_SECONDS: f32 = 40.0;
impl Showcase {
fn preset(&self) -> TreePreset {
TreePreset::ALL[self.preset]
}
}
#[derive(Component)]
struct OrbitCamera {
target: Vec3,
yaw: f32,
pitch: f32,
distance: f32,
min_distance: f32,
max_distance: f32,
framed_height: f32,
}
#[derive(Component)]
struct Hud;
fn main() {
App::new()
.add_plugins((DefaultPlugins, FloraPlugin))
.insert_resource(ClearColor(Color::srgb(0.55, 0.72, 0.92)))
.insert_resource(GlobalAmbientLight {
color: Color::srgb(0.6, 0.6, 0.6),
brightness: 300.0,
..default()
})
.insert_resource(Showcase { preset: 0, seed: 42, year: 0.3, year_running: false })
.add_systems(Startup, setup)
.add_systems(
Update,
(handle_keys, replant, advance_year, frame_tree, orbit_camera, update_hud, draw_shape).chain(),
)
.run();
}
fn setup(mut commands: Commands, showcase: Res<Showcase>) {
commands.spawn((
DirectionalLight {
shadow_maps_enabled: true,
illuminance: 10_000.0,
..default()
},
Transform::from_xyz(10.0, 20.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
));
let mut params = showcase.preset().params();
params.seed = showcase.seed;
commands.spawn((ProceduralTree(params), TreeAge(0.0), Growing { rate: 1.0 }));
let camera = OrbitCamera {
target: Vec3::new(0.0, 2.5, 0.0),
yaw: PI / 4.0,
pitch: PI / 5.0,
distance: 30.0,
min_distance: 5.0,
max_distance: 100.0,
framed_height: 0.0,
};
commands.spawn((Camera3d::default(), camera_transform(&camera), camera));
commands.spawn((
Hud,
Text::new(""),
Node {
position_type: PositionType::Absolute,
top: px(12),
left: px(12),
..default()
},
));
}
fn camera_transform(cam: &OrbitCamera) -> Transform {
let offset = Vec3::new(
cam.pitch.cos() * cam.yaw.sin(),
cam.pitch.sin(),
cam.pitch.cos() * cam.yaw.cos(),
) * cam.distance;
Transform::from_translation(cam.target + offset).looking_at(cam.target, Vec3::Y)
}
#[allow(clippy::type_complexity)]
fn handle_keys(
keys: Res<ButtonInput<KeyCode>>,
mut showcase: ResMut<Showcase>,
mut commands: Commands,
tree: Single<(
Entity,
&mut ProceduralTree,
&mut TreeAge,
Option<&Growing>,
Has<GpuGrowth>,
Has<TreeShape>,
Option<&Felled>,
Option<&TreeNotch>,
)>,
camera: Single<&Transform, With<Camera3d>>,
mut shakes: ResMut<TreeShakes>,
time: Res<Time>,
) {
let (entity, mut tree, mut age, growing, gpu, shaped, felled, notch) = tree.into_inner();
if let Some(felled) = felled {
if keys.just_pressed(KeyCode::KeyB) {
if felled.at_rest {
commands.entity(entity).insert(BuckTree::default());
}
} else if keys.get_just_pressed().next().is_some() {
commands.entity(felled.stump).despawn();
commands.entity(entity).despawn();
let mut params = showcase.preset().params();
params.seed = showcase.seed;
commands.spawn((ProceduralTree(params), TreeAge(f32::INFINITY)));
}
return;
}
let count = TreePreset::ALL.len();
let mut preset_changed = false;
if keys.just_pressed(KeyCode::BracketRight) {
showcase.preset = (showcase.preset + 1) % count;
preset_changed = true;
}
if keys.just_pressed(KeyCode::BracketLeft) {
showcase.preset = (showcase.preset + count - 1) % count;
preset_changed = true;
}
if keys.just_pressed(KeyCode::KeyN) {
showcase.seed += 1;
tree.0.seed = showcase.seed;
}
if preset_changed {
let mut params = showcase.preset().params();
params.seed = showcase.seed;
tree.0 = params;
age.0 = 0.0;
}
if keys.just_pressed(KeyCode::Space) {
if growing.is_some() {
commands.entity(entity).remove::<Growing>();
} else {
commands.entity(entity).insert(Growing { rate: 1.0 });
}
}
if keys.just_pressed(KeyCode::KeyR) {
age.0 = 0.0;
}
if keys.just_pressed(KeyCode::KeyM) {
age.0 = f32::INFINITY;
}
if keys.just_pressed(KeyCode::KeyG) {
tree.set_changed();
}
if keys.just_pressed(KeyCode::KeyU) {
if gpu {
commands.entity(entity).remove::<GpuGrowth>();
} else {
commands.entity(entity).insert(GpuGrowth);
}
}
if keys.just_pressed(KeyCode::KeyC) {
if shaped {
commands.entity(entity).remove::<TreeShape>();
} else {
commands.entity(entity).insert(TreeShape::default());
}
}
if keys.just_pressed(KeyCode::KeyY) {
showcase.year_running = !showcase.year_running;
}
if keys.just_pressed(KeyCode::KeyH) {
let hit = TreeShake { at: Vec2::ZERO, radius: 1.0, strength: 2.0, direction: Vec2::X, ..default() };
shakes.shake(hit, &time);
}
let right = camera.right();
let right = Vec2::new(right.x, right.z);
if keys.just_pressed(KeyCode::KeyX) {
let notch = notch.copied().unwrap_or(TreeNotch { direction: right, depth: 0.0, ..default() });
let depth = notch.depth + 0.08;
commands.entity(entity).insert(TreeNotch { depth, ..notch });
let blow = TreeShake { at: Vec2::ZERO, radius: 1.0, strength: 1.5, direction: -notch.direction, duration: 1.5, ..default() };
shakes.shake(blow, &time);
if depth >= 0.3 {
commands.entity(entity).insert(FellTree { direction: notch.direction, ..default() });
}
}
if keys.just_pressed(KeyCode::KeyF) {
commands.entity(entity).insert(FellTree { direction: right, ..default() });
}
}
type Leftovers = Or<(With<TreeLog>, With<TreeStump>)>;
fn replant(
keys: Res<ButtonInput<KeyCode>>,
showcase: Res<Showcase>,
mut commands: Commands,
trees: Query<(), With<ProceduralTree>>,
leftovers: Query<Entity, Leftovers>,
) {
if trees.is_empty() && keys.get_just_pressed().next().is_some() {
for entity in &leftovers {
commands.entity(entity).despawn();
}
let mut params = showcase.preset().params();
params.seed = showcase.seed;
commands.spawn((ProceduralTree(params), TreeAge(f32::INFINITY)));
}
}
fn advance_year(time: Res<Time>, mut showcase: ResMut<Showcase>, mut season: ResMut<FloraSeason>) {
if showcase.year_running {
showcase.year = (showcase.year + time.delta_secs() / YEAR_SECONDS).fract();
*season = FloraSeason::of_year(showcase.year);
}
}
fn frame_tree(mut generated: MessageReader<TreeGenerated>, mut camera: Single<&mut OrbitCamera>) {
for msg in generated.read() {
if (msg.height - camera.framed_height).abs() > 0.5 {
camera.framed_height = msg.height;
camera.target = Vec3::new(0.0, msg.height / 2.0, 0.0);
camera.distance = (msg.height * 1.5).clamp(camera.min_distance, camera.max_distance);
}
}
}
fn orbit_camera(
buttons: Res<ButtonInput<MouseButton>>,
motion: Res<AccumulatedMouseMotion>,
scroll: Res<AccumulatedMouseScroll>,
camera: Single<(&mut OrbitCamera, &mut Transform)>,
) {
let (mut cam, mut transform) = camera.into_inner();
if buttons.pressed(MouseButton::Right) {
cam.yaw -= motion.delta.x * 0.005;
cam.pitch = (cam.pitch + motion.delta.y * 0.005).clamp(-PI / 2.0 + 0.1, PI / 2.0 - 0.1);
}
if scroll.delta.y != 0.0 {
cam.distance = (cam.distance - scroll.delta.y * 2.0).clamp(cam.min_distance, cam.max_distance);
}
*transform = camera_transform(&cam);
}
#[allow(clippy::type_complexity)]
fn update_hud(
showcase: Res<Showcase>,
season: Res<FloraSeason>,
tree: Option<Single<(&ProceduralTree, &TreeAge, Option<&Growing>, Has<GpuGrowth>, Option<&TreeShape>)>>,
logs: Query<(), With<TreeLog>>,
mut hud: Single<&mut Text, With<Hud>>,
) {
let Some(tree) = tree else {
hud.0 = format!("the tree lies bucked into {} logs\n\nany key: plant a new one", logs.iter().count());
return;
};
let (tree, age, growing, gpu, shape) = tree.into_inner();
let maturity = tree.0.growth.maturity_age;
let age_text = if age.0.is_finite() {
format!("{:.1}", age.0)
} else {
"mature".to_string()
};
let shape_text = shape.map_or("off".to_string(), |shape| {
format!("{} capsules, {:.1} m tall", shape.capsules().count(), shape.height())
});
let leaves = if tree.0.foliage.autumn.is_none() {
"evergreen".to_string()
} else {
format!("turned {:.0}%, fallen {:.0}%", season.autumn * 100.0, season.leaf_fall * 100.0)
};
let season_text = format!("{leaves}, snow {:.0}%", season.snow * 100.0);
hud.0 = format!(
"preset: {} ({}/{})\nseed: {}\nage: {age_text} / {maturity}\ngrowing: {}\ngrowth: {}\ncolliders: {shape_text}\n\
season: {season_text}{}\n\n\
right-drag: orbit wheel: zoom\n[ / ]: preset N: reseed\n\
Space: toggle growing R: restart M: mature G: rebuild\n\
U: grow in the vertex shader or rebuild on the CPU C: show the gameplay shape\n\
Y: run the year (or stop it) H: hit the tree X: an axe's blow (the fourth fells it) F: fell it, then B: buck it into logs (any key: a new one)",
showcase.preset().name(),
showcase.preset + 1,
TreePreset::ALL.len(),
showcase.seed,
if growing.is_some() { "on" } else { "off" },
if gpu { "vertex shader (GpuGrowth)" } else { "CPU rebuild" },
if showcase.year_running { " (the year runs)" } else { "" },
);
}
fn draw_shape(mut gizmos: Gizmos, shapes: Query<(&GlobalTransform, &TreeShape)>) {
for (transform, shape) in &shapes {
let (_, rotation, translation) = transform.to_scale_rotation_translation();
let tree = Isometry3d::new(translation, rotation);
for capsule in shape.capsules() {
let (primitive, local) = capsule.to_primitive();
gizmos.primitive_3d(&primitive, tree * local, Color::srgb(1.0, 0.45, 0.05));
}
if let Some((center, half)) = shape.crown {
let cuboid = Cuboid { half_size: half };
gizmos.primitive_3d(&cuboid, tree * Isometry3d::from_translation(center), Color::srgb(0.1, 0.9, 1.0));
}
}
}