mod common;
use std::time::Duration;
use bevy::camera::Exposure;
use bevy::prelude::*;
use bevy_elements::*;
use common::{ExampleHarness, camera};
const RAIN_S: f32 = 45.0;
const SNOW_S: f32 = 75.0;
fn env_f64(name: &str) -> Option<f64> {
std::env::var(name).ok().and_then(|v| v.parse().ok())
}
fn fixed_preset() -> Option<WeatherPreset> {
let name = std::env::var("ELEMENTS_PRESET").ok()?;
WeatherPreset::ALL
.into_iter()
.find(|p| format!("{p:?}").eq_ignore_ascii_case(&name))
}
const SNOW_SEASON: SnowSeason = SnowSeason {
first: (11, 14),
last: (3, 31),
};
fn november() -> bool {
std::env::var("ELEMENTS_CALENDAR").is_ok_and(|v| v.eq_ignore_ascii_case("november"))
}
fn main() {
let hour = env_f64("ELEMENTS_HOUR").unwrap_or(13.0);
let scale = env_f64("ELEMENTS_SCALE").unwrap_or(300.0) as f32;
let mut app = App::new();
app.add_plugins((DefaultPlugins, ElementsPlugins, ExampleHarness))
.add_systems(Startup, setup);
if november() {
let season = std::env::var("ELEMENTS_SNOW_SEASON").map_or(true, |v| v != "off");
app.insert_resource(
WorldClock::new(CivilDate::new(2026, 11, 1))
.at_hour(hour)
.with_time_scale(scale),
)
.insert_resource(GeoLocation {
latitude_deg: 42.5,
..default()
})
.insert_resource(Climate {
mean_temperature_c: 9.4,
annual_amplitude_c: 10.0,
diurnal_amplitude_c: 4.0,
snow_season: season.then_some(SNOW_SEASON),
..Climate::temperate().with_seed(1)
});
} else {
app.insert_resource(
WorldClock::new(CivilDate::new(2026, 4, 15))
.at_hour(hour)
.with_time_scale(scale),
)
.insert_resource(Climate::temperate());
if fixed_preset().is_none() {
app.add_systems(Update, cycle.before(ElementsSystems::Clock));
}
}
#[cfg(feature = "serde")]
save::plugin(&mut app);
app.run();
}
#[cfg(feature = "serde")]
mod save {
use bevy::prelude::*;
use bevy_elements::*;
type SaveFile = (
WorldClock,
Climate,
GeoLocation,
WeatherDirector,
SurfaceWeathering,
);
pub fn plugin(app: &mut App) {
if let Some(path) = std::env::var_os("ELEMENTS_LOAD") {
let file = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("ELEMENTS_LOAD {path:?}: {e}"));
let (mut clock, climate, location, director, ground): SaveFile =
serde_json::from_str(&file)
.unwrap_or_else(|e| panic!("ELEMENTS_LOAD {path:?}: {e}"));
if let Some(scale) = super::env_f64("ELEMENTS_SCALE") {
clock.time_scale = scale as f32;
}
app.insert_resource(clock)
.insert_resource(climate)
.insert_resource(location)
.insert_resource(director)
.insert_resource(ground);
}
if std::env::var_os("ELEMENTS_SAVE").is_some() {
app.add_systems(Last, write_on_exit);
}
}
fn write_on_exit(mut exits: MessageReader<AppExit>, world: &World) {
if exits.read().next().is_none() {
return;
}
let Some(path) = std::env::var_os("ELEMENTS_SAVE") else {
return;
};
let file = serde_json::to_string(&(
world.resource::<WorldClock>(),
world.resource::<Climate>(),
world.resource::<GeoLocation>(),
world.resource::<WeatherDirector>(),
world.resource::<SurfaceWeathering>(),
))
.unwrap_or_else(|e| panic!("ELEMENTS_SAVE: {e}"));
std::fs::write(&path, file).unwrap_or_else(|e| panic!("ELEMENTS_SAVE {path:?}: {e}"));
info!("saved to {path:?}");
}
}
fn cycle(
time: Res<Time<Real>>,
mut director: ResMut<WeatherDirector>,
mut current: Local<Option<WeatherPreset>>,
) {
let t = time.elapsed_secs() % (RAIN_S + SNOW_S);
let want = if t < RAIN_S {
WeatherPreset::Rain
} else {
WeatherPreset::Snow
};
if *current != Some(want) {
director.force(want, Duration::from_secs(8));
*current = Some(want);
}
}
struct Builder<'a> {
commands: Commands<'a, 'a>,
meshes: &'a mut Assets<Mesh>,
materials: &'a mut Assets<WeatheredMaterial>,
}
impl Builder<'_> {
fn material(
&mut self,
color: Color,
roughness: f32,
metallic: f32,
exposure: f32,
) -> Handle<WeatheredMaterial> {
self.materials.add(WeatheredMaterial {
base: StandardMaterial {
base_color: color,
perceptual_roughness: roughness,
metallic,
..default()
},
extension: WeatheredExtension::with_exposure(exposure),
})
}
fn cuboid(&mut self, size: Vec3, at: Vec2, material: Handle<WeatheredMaterial>) -> Entity {
self.commands
.spawn((
Mesh3d(self.meshes.add(Cuboid::new(size.x, size.y, size.z))),
MeshMaterial3d(material),
Transform::from_xyz(at.x, size.y * 0.5, at.y),
))
.id()
}
fn house(&mut self, size: Vec3, at: Vec2, wall: Color, roof: Color) {
let wall = self.material(wall, 0.9, 0.0, 1.0);
self.cuboid(size, at, wall);
let roof = self.material(roof, 0.85, 0.0, 1.0);
let theta = 35.0_f32.to_radians();
let overhang = 0.5;
let slab = Vec3::new(
size.x + 2.0 * overhang,
0.25,
(size.z * 0.5 + overhang) / theta.cos(),
);
let rise = (size.z * 0.5) * theta.tan();
for side in [-1.0_f32, 1.0] {
self.commands.spawn((
Mesh3d(self.meshes.add(Cuboid::new(slab.x, slab.y, slab.z))),
MeshMaterial3d(roof.clone()),
Transform::from_xyz(
at.x,
size.y + rise * 0.5 + 0.125 / theta.cos(),
at.y + side * size.z * 0.25,
)
.with_rotation(Quat::from_rotation_x(side * theta)),
));
}
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WeatheredMaterial>>,
) {
let (eye, target) = match std::env::var("ELEMENTS_VIEW").as_deref() {
Ok("wall") => (Vec3::new(-12.0, 2.5, 8.0), Vec3::new(-12.0, 2.5, 22.0)),
_ => (Vec3::new(0.0, 12.0, -20.0), Vec3::new(0.0, 2.0, 26.0)),
};
let mut view = commands.spawn(camera(eye, target));
if let Some(ev100) = env_f64("ELEMENTS_EV100") {
view.insert((
ElementsCamera {
auto_exposure: false,
..default()
},
Exposure {
ev100: ev100 as f32,
},
));
}
if let Some(preset) = fixed_preset() {
commands.queue(move |world: &mut World| {
world
.resource_mut::<WeatherDirector>()
.force(preset, Duration::ZERO);
});
}
let mut b = Builder {
commands: commands.reborrow(),
meshes: &mut meshes,
materials: &mut materials,
};
let ground = b.material(Color::srgb(0.30, 0.34, 0.22), 0.95, 0.0, 1.0);
b.commands.spawn((
Mesh3d(b.meshes.add(Cuboid::new(40_000.0, 0.2, 40_000.0))),
MeshMaterial3d(ground),
Transform::from_xyz(0.0, -0.1, 0.0),
));
b.house(
Vec3::new(9.0, 5.0, 7.0),
Vec2::new(-12.0, 24.0),
Color::srgb(0.55, 0.26, 0.20),
Color::srgb(0.22, 0.20, 0.20),
);
b.house(
Vec3::new(7.0, 8.0, 7.0),
Vec2::new(-26.0, 36.0),
Color::srgb(0.72, 0.64, 0.48),
Color::srgb(0.30, 0.16, 0.12),
);
b.house(
Vec3::new(5.0, 3.0, 4.0),
Vec2::new(-20.0, 12.0),
Color::srgb(0.38, 0.27, 0.16),
Color::srgb(0.20, 0.20, 0.22),
);
let metal = b.material(Color::srgb(0.65, 0.66, 0.70), 0.35, 1.0, 1.0);
b.cuboid(Vec3::new(2.5, 2.0, 2.5), Vec2::new(16.0, 12.0), metal);
let glazed = b.material(Color::srgb(0.20, 0.45, 0.60), 0.35, 0.0, 1.0);
b.cuboid(Vec3::new(2.5, 2.0, 2.5), Vec2::new(20.0, 12.0), glazed);
let sheltered = b.material(Color::srgb(0.55, 0.26, 0.20), 0.9, 0.0, 0.0);
b.cuboid(
Vec3::new(4.0, 2.5, 4.0),
Vec2::new(8.0, 22.0),
sheltered.clone(),
);
for (dx, dz) in [(-5.0, -5.0), (5.0, -5.0), (-5.0, 5.0), (5.0, 5.0)] {
b.cuboid(
Vec3::new(0.4, 4.0, 0.4),
Vec2::new(8.0 + dx, 22.0 + dz),
sheltered.clone(),
);
}
let roof_material = b.material(Color::srgb(0.40, 0.40, 0.42), 0.85, 0.0, 1.0);
let roof_half = Vec3::new(5.6, 0.2, 5.6);
b.commands.spawn((
Mesh3d(b.meshes.add(Cuboid::new(
roof_half.x * 2.0,
roof_half.y * 2.0,
roof_half.z * 2.0,
))),
MeshMaterial3d(roof_material),
Transform::from_xyz(8.0, 4.2, 22.0),
PrecipitationBlocker {
half_extents: roof_half,
},
));
let open = b.material(Color::srgb(0.55, 0.26, 0.20), 0.9, 0.0, 1.0);
b.cuboid(Vec3::new(4.0, 2.5, 4.0), Vec2::new(-1.0, 22.0), open);
}