Week 15 of 2024
Development log of Otterhide
12 items
- Implement a buildings inspector panel
- People will move out of overcrowded houses
- Treat moving out as a historical event
- Use flatten() instead of filter_map(identity)
- Ensure that moving out probability is less than .5
- Remove unused import
- Improve camera control by using RTS camera plugin
- SQLite: Assume simulation.ron in main directory
- Create and use the snow globe model with mountains
- Fill snowglobe with trees
- Remove a stale comment
- Fix trees not restored when time traveling
Implement a buildings inspector panel
On by
index 7fe072e..aae685f 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -284,7 +284,7 @@ impl BuildingBundle {
=#[derive(
= Component, Hash, PartialEq, Eq, Clone, Debug, From, Display, Reflect, Serialize, Deserialize,
=)]
-pub struct BuildingId(Uuid);
+pub struct BuildingId(pub Uuid);
=
=/// Who lives in the building?
=///index 942fef2..36bf1de 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -1,7 +1,9 @@
-use std::borrow::Borrow;
-
=use crate::buildings::Building;
+use crate::buildings::BuildingClass;
+use crate::buildings::BuildingId;
=use crate::buildings::BuildingsRegister;
+use crate::buildings::HousingCapacity;
+use crate::buildings::Residents;
=use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
=use crate::day_month::Duration;
@@ -31,7 +33,10 @@ use bevy_egui::egui::Stroke;
=use bevy_egui::EguiContexts;
=use clap::ValueEnum;
=use derive_more::Display;
+use itertools::Itertools;
=use serde::{Deserialize, Serialize};
+use std::borrow::Borrow;
+use std::convert::identity;
=
=pub struct HudPlugin;
=
@@ -51,6 +56,7 @@ impl Plugin for HudPlugin {
= .chain();
= let central_panel_system_set = (
= paint_population_panel.run_if(inspector_panel_shown(&InspectorPanel::Population)),
+ paint_buildings_panel.run_if(inspector_panel_shown(&InspectorPanel::Buildings)),
= // TODO: Implement other panels
= // TODO: A mechanism to hide central panel
= );
@@ -78,6 +84,7 @@ impl Plugin for HudPlugin {
=)]
=pub enum InspectorPanel {
= Population,
+ Buildings,
=}
=
=fn inspector_panel_shown(panel: &InspectorPanel) -> impl FnMut(Res<Configuration>) -> bool + '_ {
@@ -405,3 +412,86 @@ fn paint_population_panel(
= })
= });
=}
+
+fn paint_buildings_panel(
+ mut contexts: EguiContexts,
+ buildings: Query<
+ (
+ &BuildingId,
+ &Name,
+ &BuildingClass,
+ &HousingCapacity,
+ Option<&Residents>,
+ ),
+ With<Building>,
+ >,
+ persons: Query<(&Name, &BirthDate, &Sex), With<Person>>,
+ buildings_register: Res<BuildingsRegister>,
+ persons_register: Res<PersonsRegister>,
+
+ date: Res<Date>,
+) {
+ // TODO: DRY on inspector panels layout ...
+ egui::CentralPanel::default()
+ .frame(Frame {
+ inner_margin: Margin::symmetric(40., 20.),
+ outer_margin: Margin {
+ top: 60.,
+ left: 40.,
+ right: 40.,
+ bottom: 10.,
+ },
+ shadow: Shadow::NONE,
+ stroke: Stroke::NONE,
+ fill: Color32::from_rgba_premultiplied(20, 20, 20, 240),
+ ..default()
+ })
+ .show(contexts.ctx_mut(), |ui| {
+ // ... Down to here it should be it's own function
+ let count = buildings.into_iter().len();
+ let registered = buildings_register.0.len();
+ ui.heading(format!("The {count} ({registered}) Buildings of Otterhide"));
+ egui::ScrollArea::both().show(ui, |ui| {
+ // TODO: Use Table from egui_extras
+ egui::Grid::new("persons-grid").show(ui, |ui| {
+ ui.label("Address");
+ ui.label("Class");
+ ui.label("Capacity");
+ ui.label("Residents:");
+ ui.label("Id");
+ ui.end_row();
+
+ for (id, address, BuildingClass(class), HousingCapacity(capacity), residents) in
+ buildings.iter()
+ {
+ // let address = residence
+ // .and_then(|Residence(id)| buildings_register.0.get(id))
+ // .map(|entity| buildings.get(*entity).unwrap().as_str())
+ // .unwrap_or("-");
+
+ // let age = Duration::new(birth_date, date.0.borrow()).years();
+ let residents = residents
+ .map(|residents| residents.0.clone())
+ .unwrap_or_default()
+ .iter()
+ .map(|id| persons_register.0.get(id))
+ .filter_map(identity)
+ .map(|entity| persons.get(*entity).ok())
+ .filter_map(identity)
+ .map(|(name, birthdate, sex)| {
+ let age = Duration::new(&birthdate.0, &date.0).years();
+ format!("{sex} {age:02} {name}")
+ })
+ .join("\n");
+
+ ui.label(address.to_string());
+ ui.label(class);
+ ui.label(capacity.to_string());
+ ui.label(residents);
+ ui.label(id.0.to_string());
+ ui.end_row();
+ }
+ })
+ })
+ });
+}People will move out of overcrowded houses
On by
index aae685f..b1fcaaa 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -4,6 +4,7 @@ use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::major_state::PreloadedAssets;
=use crate::parcels::{Parcel, ParcelNumber};
+use crate::population::MovedOut;
=use crate::population::{MovedIn, Person, PersonId};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
@@ -50,6 +51,12 @@ impl Plugin for BuildingsPlugin {
= .run_if(on_event::<MovedIn>())
= .after(receive_orders),
= )
+ .add_systems(
+ Update,
+ move_out_of_houses
+ .run_if(on_event::<MovedOut>())
+ .after(move_into_houses),
+ )
= .add_systems(Update, receive_orders);
= }
=}
@@ -64,6 +71,19 @@ fn move_into_houses(
= }
=}
=
+fn move_out_of_houses(
+ mut moved_out: EventReader<MovedOut>,
+ systems: Res<BuildingsSystems>,
+ mut commands: Commands,
+) {
+ for event in moved_out.read() {
+ commands.run_system_with_input(
+ systems.remove_resident,
+ (event.where_from.clone(), event.who.clone()),
+ )
+ }
+}
+
=#[derive(Resource)]
=struct BuildingAssets(Handle<Gltf>);
=index 627d3ea..74af263 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,4 +1,4 @@
-use crate::buildings::{BuildingId, BuildingsSystems, HasSpareCapacity};
+use crate::buildings::{BuildingId, BuildingsSystems, HasSpareCapacity, IsOvercrowded, Residents};
=use crate::date::SimulationFrameCounter;
=use crate::date::{self, Date};
=use crate::day_month::{DayMonth, Duration};
@@ -30,6 +30,7 @@ impl Plugin for PopulationPlugin {
= .register_type::<PersonName>()
= .add_event::<Immigrated>()
= .add_event::<MovedIn>()
+ .add_event::<MovedOut>()
= .add_event::<Died>()
= .add_systems(Startup, register_population_systems)
= .add_systems(
@@ -44,7 +45,7 @@ impl Plugin for PopulationPlugin {
= Update,
= implement_immigration
= .run_if(on_event::<Immigrated>())
- .before(search_for_houses),
+ .before(plan_moving_into_houses),
= )
= .add_systems(PreUpdate, remove_dead)
= .add_systems(
@@ -61,7 +62,17 @@ impl Plugin for PopulationPlugin {
= .add_systems(Update, mark_dead.run_if(on_event::<Died>()))
= .add_systems(
= Update,
- search_for_houses.before(move_into_houses).run_if(
+ plan_moving_out_of_overcrowded_houses
+ .before(move_out_of_houses)
+ .run_if(
+ in_state(MajorState::Simulating)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
+ )
+ .add_systems(Update, move_out_of_houses.run_if(on_event::<MovedOut>()))
+ .add_systems(
+ Update,
+ plan_moving_into_houses.before(move_into_houses).run_if(
= in_state(MajorState::Simulating)
= .and_then(resource_changed::<SimulationFrameCounter>),
= ),
@@ -257,8 +268,13 @@ pub struct MovedIn {
= pub where_to: BuildingId,
=}
=
-// For now just assign random buildings for each homeless person, with a 50% chance
-fn search_for_houses(
+#[derive(Event, Debug, Clone, Serialize, Deserialize)]
+pub struct MovedOut {
+ pub who: PersonId,
+ pub where_from: BuildingId,
+}
+
+fn plan_moving_into_houses(
= persons: Query<&PersonId, Without<Residence>>,
= buildings: Query<&BuildingId, With<HasSpareCapacity>>,
= mut moved_in: EventWriter<MovedIn>,
@@ -285,12 +301,49 @@ fn move_into_houses(
=
= let MovedIn { who, where_to } = event;
= let Some(person) = persons_register.0.get(who) else {
+ // TODO: Don't panic. Make it so, that all events can fail without leaving the world in inconsitent state.
= panic!("Person {who:#?} not in the register! {persons_register:#?}");
= };
= commands.entity(*person).insert(Residence(where_to.clone()));
= }
=}
=
+fn plan_moving_out_of_overcrowded_houses(
+ buildings: Query<(&BuildingId, &Residents), With<IsOvercrowded>>,
+ mut moved_out: EventWriter<MovedOut>,
+) {
+ for (building, residents) in buildings.iter() {
+ let probability = residents.0.len() as f64 / 100.0;
+ for resident in residents.0.iter() {
+ if thread_rng().gen_bool(probability) {
+ moved_out.send(MovedOut {
+ who: resident.clone(),
+ where_from: building.clone(),
+ });
+ }
+ }
+ }
+}
+
+fn move_out_of_houses(
+ mut moved_out: EventReader<MovedOut>,
+ persons_register: Res<PersonsRegister>,
+ mut commands: Commands,
+) {
+ for event in moved_out.read() {
+ debug!("Moving out of a house {event:#?}");
+
+ let MovedOut { who, where_from: _ } = event;
+ let Some(person) = persons_register.0.get(who) else {
+ warn!(
+ "Person {who:#?} not in the register! {persons_register:#?}. Maybe they are dead?"
+ );
+ return;
+ };
+ commands.entity(*person).remove::<Residence>();
+ }
+}
+
=#[derive(Bundle)]
=pub struct PersonBundle {
= pub id: PersonId,Treat moving out as a historical event
On by
Write an example query in the readme file. Also, fix it's bizare casing.
new file mode 100644
index 0000000..02e5d41
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+- Run Bevy locally after setting up nix:
+ - `nix develop`
+ - `make web/develop` (default runs 8080)
+
+
+# Example queries
+
+Who lived where:
+
+``` sql
+select
+ name,
+ address,
+ concat (from_year, '-', from_month) as moved_in,
+ concat (until_year, '-', until_month) as moved_out
+from
+ residency
+ inner join person on residency.person = person.id
+ inner join building on residency.building = building.id
+;
+```deleted file mode 100644
index ba91b1d..0000000
--- a/Readme.MD
+++ /dev/null
@@ -1,3 +0,0 @@
-- Run Bevy locally after setting up nix:
- - `nix develop`
- - `make web/develop` (default runs 8080)index c2dc4d6..b1da5b5 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -3,7 +3,7 @@ use clap::Parser;
=use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
-use otterhide::population::{Died, Immigrated, MovedIn};
+use otterhide::population::{Died, Immigrated, MovedIn, MovedOut};
=use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous};
=use std::path::PathBuf;
=
@@ -152,6 +152,24 @@ async fn main() -> anyhow::Result<()> {
= .await
= .context("Inserting immigration data")?;
= }
+ HistoricalEvent::MovedOut(MovedOut { where_from, who }) => {
+ sqlx::query(
+ "Update residency set
+ until_year = ?,
+ until_month = ?
+ where
+ person = ?
+ and building = ?
+ and until_month is null"
+ )
+ .bind(date.year())
+ .bind(date.month() as u8)
+ .bind(who.to_string())
+ .bind(where_from.to_string())
+ .execute(&pool)
+ .await
+ .context("Inserting immigration data")?;
+ }
= }
= }
=index e374204..97ca1a9 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -4,7 +4,7 @@ use crate::day_month::{DayMonth, Month};
=use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
=use crate::major_state::{MajorState, ReadyPredicates};
-use crate::population::{Died, Immigrated, MovedIn, PopulationSnapshot};
+use crate::population::{Died, Immigrated, MovedIn, MovedOut, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
@@ -93,6 +93,7 @@ pub enum HistoricalEvent {
= Immigrated(Immigrated),
= Died(Died),
= MovedIn(MovedIn),
+ MovedOut(MovedOut),
=}
=
=/// A historical event together with it's date
@@ -128,6 +129,9 @@ impl Display for HistoricalEvent {
= HistoricalEvent::MovedIn(MovedIn { who, where_to }) => {
= write!(f, "The person {who} moved into the building {where_to}")
= }
+ HistoricalEvent::MovedOut(MovedOut { who, where_from }) => {
+ write!(f, "The person {who} moved out of the building {where_from}")
+ }
= HistoricalEvent::Died(Died { person: id }) => {
= write!(f, "The person {id} died")
= }
@@ -142,6 +146,7 @@ fn register_historical_events(
= mut new_districts: EventReader<NewDistrictEstablished>,
= mut immigrated: EventReader<Immigrated>,
= mut moved_in: EventReader<MovedIn>,
+ mut moved_out: EventReader<MovedOut>,
= mut died: EventReader<Died>,
=) {
= let date = date.0;
@@ -165,6 +170,11 @@ fn register_historical_events(
= debug!("Registering a historical event on {date:?}: {event:#?}");
= history.events.push(EventLogEntry { event, date });
= }
+ for event in moved_out.read() {
+ let event = HistoricalEvent::MovedOut(event.to_owned());
+ debug!("Registering a historical event on {date:?}: {event:#?}");
+ history.events.push(EventLogEntry { event, date });
+ }
= for event in died.read() {
= let event = HistoricalEvent::Died(event.to_owned());
= debug!("Registering a historical event on {date:?}: {event:#?}");
@@ -274,6 +284,7 @@ fn replay_historical_events(
= mut districts: EventWriter<NewDistrictEstablished>,
= mut immigrated: EventWriter<Immigrated>,
= mut moved_in: EventWriter<MovedIn>,
+ mut moved_out: EventWriter<MovedOut>,
= mut died: EventWriter<Died>,
=) {
= future.events.retain(|entry| {
@@ -293,6 +304,9 @@ fn replay_historical_events(
= HistoricalEvent::MovedIn(event) => {
= moved_in.send(event);
= }
+ HistoricalEvent::MovedOut(event) => {
+ moved_out.send(event);
+ }
= HistoricalEvent::Died(event) => {
= died.send(event);
= }Use flatten() instead of filter_map(identity)
On by
Following Clippy's advice.
index 36bf1de..e0f63fd 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -475,9 +475,9 @@ fn paint_buildings_panel(
= .unwrap_or_default()
= .iter()
= .map(|id| persons_register.0.get(id))
- .filter_map(identity)
+ .flatten()
= .map(|entity| persons.get(*entity).ok())
- .filter_map(identity)
+ .flatten()
= .map(|(name, birthdate, sex)| {
= let age = Duration::new(&birthdate.0, &date.0).years();
= format!("{sex} {age:02} {name}")Ensure that moving out probability is less than .5
On by
This prevents 2 problems.
-
Panic when there is more than 100 people in one building
The gen_bool function panics if given probability is more than 1.0, which could happen in extreme cases when more than 100 residents were occupying the same building.
-
Going from high occupancy to 0 in one frame
Technically it's still possible, but very unlikely now.
index 74af263..25c2fd7 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -315,7 +315,7 @@ fn plan_moving_out_of_overcrowded_houses(
= for (building, residents) in buildings.iter() {
= let probability = residents.0.len() as f64 / 100.0;
= for resident in residents.0.iter() {
- if thread_rng().gen_bool(probability) {
+ if thread_rng().gen_bool(probability.min(0.5)) {
= moved_out.send(MovedOut {
= who: resident.clone(),
= where_from: building.clone(),Remove unused import
On by
index e0f63fd..1fc935f 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -36,7 +36,6 @@ use derive_more::Display;
=use itertools::Itertools;
=use serde::{Deserialize, Serialize};
=use std::borrow::Borrow;
-use std::convert::identity;
=
=pub struct HudPlugin;
=Improve camera control by using RTS camera plugin
On by
It's from Plonq, who also authored the pan/orbit camera plugin, but better suited for our use-case. It uses a ground entity to stay "above" the ground. No need for hacky home-grown limitations.
index a496bcb..20a548d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -890,12 +890,24 @@ dependencies = [
=]
=
=[[package]]
-name = "bevy_panorbit_camera"
-version = "0.16.1"
+name = "bevy_mod_raycast"
+version = "0.17.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2ae36af26f6c75067cba3fac09fbd4cbe7fc565492cc6064338a9e0063363db"
+checksum = "9ca646aeaab4a170e1f3e8284b925e2f990eb18616e95d7826c873c8e26ee945"
=dependencies = [
- "bevy",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_gizmos",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_sprite",
+ "bevy_transform",
+ "bevy_utils",
+ "bevy_window",
+ "crossbeam-channel",
=]
=
=[[package]]
@@ -1017,6 +1029,16 @@ dependencies = [
= "syn 2.0.52",
=]
=
+[[package]]
+name = "bevy_rts_camera"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b52a24bd525131eae1f27f60b9434ed4f449714ff545bdabdaeeeae67c20ecd"
+dependencies = [
+ "bevy",
+ "bevy_mod_raycast",
+]
+
=[[package]]
=name = "bevy_scene"
=version = "0.13.0"
@@ -3382,7 +3404,7 @@ dependencies = [
= "bevy-inspector-egui",
= "bevy_args",
= "bevy_egui",
- "bevy_panorbit_camera",
+ "bevy_rts_camera",
= "clap",
= "derive_more",
= "gloo-file",index 45aede4..2cd8908 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ bevy = { version = "0.13", features = ["wayland", "serialize"] }
=bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector-egui", rev = "1563fe9", version = "0.23.4" }
=bevy_args = "=1.3.0"
=bevy_egui = "0.25.0"
-bevy_panorbit_camera = "0.16.0"
+bevy_rts_camera = "0.4.0"
=clap = { version = "4.5.4", features = ["derive"] }
=derive_more = "0.99.17"
=gloo-file = "0.3.0"index 1c3ed2b..af5f491 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -1,35 +1,49 @@
-use bevy::prelude::*;
-use bevy_panorbit_camera::PanOrbitCamera;
-use bevy_panorbit_camera::PanOrbitCameraPlugin;
-use std::f32::consts::TAU;
-
=use crate::major_state::MajorState;
+use crate::simulation;
+use crate::simulation::SimulationParameters;
+use bevy::prelude::*;
+use bevy_rts_camera::{RtsCamera, RtsCameraControls, RtsCameraPlugin};
=
=pub struct CameraPlugin;
=
=impl Plugin for CameraPlugin {
= fn build(&self, app: &mut App) {
- app.add_plugins(PanOrbitCameraPlugin)
- .add_systems(Startup, setup_camera)
- .add_systems(OnEnter(MajorState::GettingReady), disable_camera)
- .add_systems(OnExit(MajorState::GettingReady), enable_camera)
- .add_systems(Update, limit_camera);
+ app.add_plugins(RtsCameraPlugin)
+ .add_systems(
+ OnEnter(MajorState::GettingReady),
+ (setup_camera, disable_camera)
+ .chain()
+ .after(simulation::get_ready),
+ )
+ .add_systems(OnExit(MajorState::GettingReady), enable_camera);
= }
=}
=
-fn setup_camera(mut commands: Commands) {
- commands
- .spawn(Camera3dBundle::default())
- .insert(PanOrbitCamera {
- alpha: Some(0.0),
- beta: Some(1.0),
- focus: Vec3::ZERO,
- radius: Some(1000.),
- zoom_lower_limit: Some(500.),
- zoom_upper_limit: Some(2000.),
- beta_lower_limit: Some(TAU / 128.0),
+fn setup_camera(mut commands: Commands, parameters: Res<SimulationParameters>) {
+ commands.spawn((
+ Camera3dBundle::default(),
+ RtsCamera {
+ height_max: 2000.,
+ height_min: 10.,
+
+ bounds: bevy::math::bounding::Aabb2d {
+ min: Vec2 {
+ x: -parameters.land_radius,
+ y: -parameters.land_radius,
+ },
+ max: Vec2 {
+ x: parameters.land_radius,
+ y: parameters.land_radius,
+ },
+ },
= ..default()
- });
+ },
+ RtsCameraControls {
+ pan_speed: 200.,
+ edge_pan_width: 0.2,
+ ..default()
+ },
+ ));
=}
=
=fn disable_camera(mut cameras: Query<&mut Camera>) {
@@ -41,15 +55,3 @@ fn enable_camera(mut cameras: Query<&mut Camera>) {
= let mut camera = cameras.single_mut();
= camera.is_active = true;
=}
-
-// TODO: Find a better way to lock camera focus on the ground. Current solution
-// is a kludge that doesn't work very well, esp. when the camera is low.
-// Probably disable default panning system and instead control focus by mapping
-// pointer movements onto ground plane and inversing it. Part of the solution
-// might be here:
-// https://bevyengine.org/examples/3D%20Rendering/3d-viewport-to-world/
-fn limit_camera(mut cameras: Query<&mut PanOrbitCamera>) {
- for mut camera in cameras.iter_mut() {
- camera.target_focus.y = 0.0;
- }
-}index a470802..ac8ec68 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -1,6 +1,7 @@
=use crate::major_state::MajorState;
=use crate::simulation::{self, SimulationParameters};
=use bevy::prelude::*;
+use bevy_rts_camera::Ground;
=use std::f32::consts::FRAC_PI_2;
=
=pub struct GroundPlugin;
@@ -14,17 +15,14 @@ impl Plugin for GroundPlugin {
= }
=}
=
-#[derive(Component)]
-struct Ground;
-
=fn setup_ground(
= mut commands: Commands,
= mut meshes: ResMut<Assets<Mesh>>,
= mut materials: ResMut<Assets<StandardMaterial>>,
= settings: Res<SimulationParameters>,
=) {
- commands
- .spawn(PbrBundle {
+ commands.spawn((
+ PbrBundle {
= mesh: meshes.add(Circle::new(settings.land_radius)),
= material: materials.add(StandardMaterial {
= base_color: Color::hsl(150.0, 0.3, 0.3),
@@ -33,6 +31,7 @@ fn setup_ground(
= }),
= transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)),
= ..default()
- })
- .insert(Ground);
+ },
+ Ground,
+ ));
=}SQLite: Assume simulation.ron in main directory
On by
It makes it easier to run integration tests.
index b8de0ea..63129cf 100644
--- a/Makefile
+++ b/Makefile
@@ -60,7 +60,7 @@ check/sqlite-export/watch:
= watchexec \
= --watch src/ \
= --debounce 1s \
- 'make check/sqlite-export --assume-new assets/simulation.ron'
+ 'make check/sqlite-export --assume-new simulation.ron'
=.PHONY: check/sqlite-export/watch
=
=check/sqlite-export/schema: ## Check if the SQLite schema file is valid
@@ -85,7 +85,7 @@ data/sqlite-export.sql: data/sqlite-export.db
= mkdir --parents $(@D)
= sqlite3 $< .dump > $@
=
-data/sqlite-export.db: assets/simulation.ron
+data/sqlite-export.db: simulation.ron
=data/sqlite-export.db: check/sqlite-export/schema
=data/sqlite-export.db: src/**/*
=data/sqlite-export.db:Create and use the snow globe model with mountains
On by
new file mode 100644
index 0000000..cfb6b46
Binary files /dev/null and b/art/snowglobe.blend differnew file mode 100644
index 0000000..1457be2
Binary files /dev/null and b/assets/snowglobe.glb differindex af5f491..96c0ac0 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -23,8 +23,12 @@ fn setup_camera(mut commands: Commands, parameters: Res<SimulationParameters>) {
= commands.spawn((
= Camera3dBundle::default(),
= RtsCamera {
- height_max: 2000.,
- height_min: 10.,
+ height_max: 3000.,
+ // TODO: Lower the camera once ground position is back a 0
+ //
+ // This value compensates for the logical "Ground" entity being
+ // under the visual ground. See the ground.rs for more comments.
+ height_min: 20.,
=
= bounds: bevy::math::bounding::Aabb2d {
= min: Vec2 {index ac8ec68..e68651c 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -17,10 +17,27 @@ impl Plugin for GroundPlugin {
=
=fn setup_ground(
= mut commands: Commands,
+ asset_server: Res<AssetServer>,
= mut meshes: ResMut<Assets<Mesh>>,
= mut materials: ResMut<Assets<StandardMaterial>>,
= settings: Res<SimulationParameters>,
=) {
+ let snowglobe = asset_server.load("snowglobe.glb#Scene0");
+
+ commands.spawn((
+ SceneBundle {
+ scene: snowglobe,
+ transform: Transform::from_scale(Vec3 {
+ x: 100.0,
+ y: 100.0,
+ z: 100.0,
+ }),
+ ..default()
+ },
+ SnowGlobe,
+ ));
+
+ // TODO: Use the ground entity from the snow globe model
= commands.spawn((
= PbrBundle {
= mesh: meshes.add(Circle::new(settings.land_radius)),
@@ -29,9 +46,17 @@ fn setup_ground(
= perceptual_roughness: 0.8,
= ..default()
= }),
- transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)),
+ // NOTE: Setting ground visibility to hidden breaks the camera work : (
+ // So instead I set it to be 4m under the visual ground. It's
+ // a hack that propagates to camera.rs code.
+ transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2))
+ .with_translation(-4.0 * Vec3::Y),
+ // visibility: Visibility::Hidden,
= ..default()
= },
= Ground,
= ));
=}
+
+#[derive(Component, Debug)]
+struct SnowGlobe;index eb59ab3..6267bd4 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -53,7 +53,7 @@ fn lay_initial_roads(
= info!("Laying initial roads.");
=
= let center = Coordinates::default();
- let length = settings.land_radius as u32 / 10;
+ let length = (settings.land_radius as u32 + 500) / 10;
= let mut plan = RoadPlan::default();
=
= for direction in [Fill snowglobe with trees
On by
The trees are scattered in Blender model using geometry nodes. When roads or districts are created, the trees are removed in their place.
index cfb6b46..2062eba 100644
Binary files a/art/snowglobe.blend and b/art/snowglobe.blend differnew file mode 100644
index 0000000..43b9b4c
Binary files /dev/null and b/art/trees.blend differindex 1457be2..e9096b1 100644
Binary files a/assets/snowglobe.glb and b/assets/snowglobe.glb differindex 5d904c9..60edb38 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,6 +1,7 @@
=use crate::buildings::Building;
=use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
+use crate::ground::GroundSystems;
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::major_state::PreloadedAssets;
@@ -11,6 +12,7 @@ use crate::roads::{RoadPlan, RoadsSystems};
=use crate::simulation::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
+use bevy::math::bounding::Aabb3d;
=use bevy::prelude::*;
=use bevy::utils::{HashMap, HashSet};
=use itertools::Itertools;
@@ -110,7 +112,6 @@ fn setup_districts(
=) {
= let districts_gltf = gltf_assets.get(&districts_asset_handle.0).unwrap();
=
- // TODO: Do it for every scene
= for (name, scene_handle) in districts_gltf.named_scenes.iter() {
= debug!("Processing district {name}");
= let Ok(variant) = name.parse::<DistrictVariant>() else {
@@ -263,6 +264,19 @@ pub struct DistrictDescription {
= pub name: Name,
=}
=
+impl From<&DistrictDescription> for Aabb3d {
+ fn from(value: &DistrictDescription) -> Self {
+ Self::new(
+ value.center(),
+ Vec3 {
+ x: (value.length() * 5) as f32,
+ y: 30.0,
+ z: (value.width() * 5) as f32,
+ },
+ )
+ }
+}
+
=#[derive(Component, Debug, Clone, Copy, Serialize, Deserialize)]
=pub enum Rotation {
= None = 0,
@@ -578,9 +592,13 @@ fn implement_new_districts(
= mut commands: Commands,
= mut new_districts: EventReader<NewDistrictEstablished>,
= systems: Res<DistrictsSystems>,
+ ground_systems: Res<GroundSystems>,
=) {
= for NewDistrictEstablished(district) in new_districts.read() {
= commands.run_system_with_input(systems.construct_district, district.clone());
+
+ let bound = Aabb3d::from(district);
+ commands.run_system_with_input(ground_systems.remove_obstacles, bound);
= }
=}
=index e68651c..200ddfa 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -1,32 +1,144 @@
-use crate::major_state::MajorState;
+use crate::major_state::{MajorState, PreloadedAssets};
=use crate::simulation::{self, SimulationParameters};
+use bevy::ecs::system::SystemId;
+use bevy::gltf::Gltf;
+use bevy::math::bounding::{Aabb3d, IntersectsVolume};
=use bevy::prelude::*;
=use bevy_rts_camera::Ground;
+use itertools::Itertools;
=use std::f32::consts::FRAC_PI_2;
=
=pub struct GroundPlugin;
=
=impl Plugin for GroundPlugin {
= fn build(&self, app: &mut App) {
- app.add_systems(
- OnEnter(MajorState::GettingReady),
- setup_ground.after(simulation::get_ready),
+ app.add_systems(Startup, register_ground_systems)
+ .register_type::<Tree>()
+ // .add_systems(Update, draw_gizmos)
+ .add_systems(Startup, setup_assets)
+ .add_systems(
+ OnEnter(MajorState::GettingReady),
+ setup_ground.after(simulation::get_ready),
+ );
+ }
+}
+
+#[derive(Resource, Debug)]
+pub struct GroundSystems {
+ pub remove_obstacles: SystemId<Aabb3d>,
+}
+
+fn register_ground_systems(world: &mut World) {
+ let remove_obstacles = world.register_system(remove_obstacles);
+
+ world.insert_resource(GroundSystems { remove_obstacles });
+}
+
+fn remove_obstacles(
+ In(aabb): In<Aabb3d>,
+ obstacles: Query<(Entity, &GlobalTransform), With<Tree>>,
+ mut commands: Commands,
+) {
+ debug!("Removing obstacles in {aabb:#?}");
+
+ let mut left = 0;
+ let mut removed = 0;
+
+ for (entity, transform) in obstacles.iter() {
+ let obstacle_bound = Aabb3d::new(
+ transform.translation(),
+ Vec3 {
+ x: 1.0,
+ y: 10.0,
+ z: 1.0,
+ },
= );
+ if aabb.intersects(&obstacle_bound) {
+ debug!("Intersecting {obstacle_bound:#?} and {aabb:#?}. Removing obstacle #{entity:?}");
+ removed += 1;
+ commands.entity(entity).despawn_recursive();
+ } else {
+ left += 1;
+ }
= }
+ debug!("Removed {removed} obstacles. Left {left}.")
=}
=
+fn draw_gizmos(mut gizmos: Gizmos, trees: Query<&GlobalTransform, With<Tree>>) {
+ for transform in trees.iter() {
+ gizmos.cuboid(
+ Transform::from_translation(transform.translation()).with_scale(Vec3 {
+ x: 3.0,
+ y: 20.0,
+ z: 3.0,
+ }),
+ Color::GREEN,
+ );
+ }
+}
+
+#[derive(Resource)]
+struct SnowglobeAssetHandle(Handle<Gltf>);
+
+fn setup_assets(
+ assets: Res<AssetServer>,
+ mut commands: Commands,
+ mut preloaded: ResMut<PreloadedAssets>,
+) {
+ const ASSET_PATH: &str = "snowglobe.glb";
+ info!("Loading {ASSET_PATH} asset");
+ let handle = assets.load(ASSET_PATH);
+ preloaded.0.insert(handle.clone().untyped());
+ commands.insert_resource(SnowglobeAssetHandle(handle));
+}
+
+#[derive(Component, Debug, Reflect)]
+#[reflect(Component)]
+struct Tree;
+
=fn setup_ground(
+ snowglobe_asset_handle: Res<SnowglobeAssetHandle>,
+ gltf_assets: Res<Assets<Gltf>>,
+ mut scenes_assets: ResMut<Assets<Scene>>,
= mut commands: Commands,
- asset_server: Res<AssetServer>,
= mut meshes: ResMut<Assets<Mesh>>,
= mut materials: ResMut<Assets<StandardMaterial>>,
= settings: Res<SimulationParameters>,
=) {
- let snowglobe = asset_server.load("snowglobe.glb#Scene0");
+ debug!("Processing snowglobe scene");
+
+ let snowglobe_gltf = gltf_assets.get(&snowglobe_asset_handle.0).unwrap();
+ let scene_handle = snowglobe_gltf.scenes.first().unwrap();
+ let scene = scenes_assets.get_mut(scene_handle.clone()).unwrap();
+
+ let trees = scene
+ .world
+ .query::<(Entity, &Name)>()
+ .iter(&scene.world)
+ .filter_map(|(entity, name)| {
+ // TODO: Find a way to give instances more meaningful names
+ // The GN Instance comes from Blender's GLTF export. Any
+ // instance created with geometry nodes will have this name. I
+ // could not find a way to change it.
+ if name.as_str() == "GN Instance" {
+ Some(entity)
+ } else {
+ None
+ }
+ })
+ .collect_vec();
+
+ for entity in trees {
+ scene
+ .world
+ .entity_mut(entity)
+ .insert(Tree)
+ .insert(Name::new("Tree"));
+ }
=
= commands.spawn((
= SceneBundle {
- scene: snowglobe,
+ scene: scene_handle.clone(),
= transform: Transform::from_scale(Vec3 {
= x: 100.0,
= y: 100.0,
@@ -35,6 +147,7 @@ fn setup_ground(
= ..default()
= },
= SnowGlobe,
+ Name::new("Snow globe"),
= ));
=
= // TODO: Use the ground entity from the snow globe model
@@ -55,6 +168,7 @@ fn setup_ground(
= ..default()
= },
= Ground,
+ Name::new("Ground"),
= ));
=}
=index 6267bd4..e093f4a 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,12 +1,12 @@
=use crate::coordinates::{Coordinates, Direction};
-
+use crate::ground::GroundSystems;
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::major_state::PreloadedAssets;
-use crate::simulation;
=use crate::simulation::SimulationParameters;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
+use bevy::math::bounding::Aabb3d;
=use bevy::prelude::*;
=use bevy::utils::hashbrown::HashSet;
=use bevy::utils::HashMap;
@@ -21,15 +21,25 @@ pub struct RoadsPlugin;
=impl Plugin for RoadsPlugin {
= fn build(&self, app: &mut App) {
= app.init_resource::<Roads>()
+ // .add_systems(Update, draw_gizmos)
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_road_systems)
= .add_systems(
- OnEnter(MajorState::GettingReady),
- lay_initial_roads.after(simulation::get_ready),
+ OnExit(MajorState::GettingReady),
+ lay_initial_roads, // .after(simulation::get_ready)
= );
= }
=}
=
+fn draw_gizmos(mut gizmos: Gizmos, sections: Res<Roads>) {
+ for coordinates in sections.sections.keys() {
+ gizmos.cuboid(
+ Transform::from_translation(Vec3::from(coordinates)).with_scale(Vec3::splat(10.0)),
+ Color::RED,
+ );
+ }
+}
+
=#[derive(Resource)]
=struct RoadAssets(Handle<Gltf>);
=
@@ -94,6 +104,7 @@ fn construct_road_section(
= assets: Res<Assets<Gltf>>,
= mut roads: ResMut<Roads>,
= mut sections: Query<&RoadSection>,
+ ground_systems: Res<GroundSystems>,
=) {
= if let Some(gltf) = assets.get(&road_assets.0) {
= match roads.sections.get(&coordinates) {
@@ -135,6 +146,9 @@ fn construct_road_section(
= }
= }
= };
+
+ let bound = Aabb3d::new(coordinates.borrow().into(), Vec3::splat(10.0));
+ commands.run_system_with_input(ground_systems.remove_obstacles, bound);
=}
=
=fn implement_road_plan(In(plan): In<RoadPlan>, mut commands: Commands, systems: Res<RoadsSystems>) {Remove a stale comment
On by
index 97ca1a9..026bc10 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -70,9 +70,6 @@ pub struct Future {
= pub events: Vec<EventLogEntry>,
=}
=
-// TODO: If the Snapshot trait experiment works, move it to the history module
-// The idea is to use the trait directly, by getting all types that
-// implement it and calling their methods. Also rename it to Snapshot.
=pub trait Snapshot {
= /// Given a reference to the world, produce and return a snapshot
= ///Fix trees not restored when time traveling
On by
The trees were permanently removed for roads and district construction, but when traveling to the time before their removal, there was no mechanism to bring them back.
Given the amount of trees it's not feasible to store them in snapshots. Another option would be to re-spawn the whole ground. But I opted to, instead of removing them, set visibility to hidden. So the tree entities remain after removal, they are just not visible. Then, when restoring a snapshot, all visibility is restored.
The downside of this aproach is that it depends on the order of snapshots restoration. Basically ground has to be restored before any snapshots that may remove the trees again (currently these are roads and districts).
In debug builds this operation is quite slow. In release it goes smooth on my laptop, both native and web build.
index 60edb38..ea04b16 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -592,13 +592,9 @@ fn implement_new_districts(
= mut commands: Commands,
= mut new_districts: EventReader<NewDistrictEstablished>,
= systems: Res<DistrictsSystems>,
- ground_systems: Res<GroundSystems>,
=) {
= for NewDistrictEstablished(district) in new_districts.read() {
= commands.run_system_with_input(systems.construct_district, district.clone());
-
- let bound = Aabb3d::from(district);
- commands.run_system_with_input(ground_systems.remove_obstacles, bound);
= }
=}
=
@@ -645,6 +641,7 @@ fn construct_district(
= In(description): In<DistrictDescription>,
= scenes: Res<DistrictScenes>,
= roads_systems: Res<RoadsSystems>,
+ ground_systems: Res<GroundSystems>,
= mut commands: Commands,
=) {
= debug!("Spawning a district: {description:#?}");
@@ -653,6 +650,10 @@ fn construct_district(
= roads_systems.implement_road_plan,
= description.plan_border_roads(),
= );
+
+ let bound = Aabb3d::from(&description);
+ commands.run_system_with_input(ground_systems.remove_obstacles, bound);
+
= commands.spawn(DistrictBundle::new(description, &scenes));
=}
=index 200ddfa..0d9813d 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -1,3 +1,4 @@
+use crate::history::Snapshot;
=use crate::major_state::{MajorState, PreloadedAssets};
=use crate::simulation::{self, SimulationParameters};
=use bevy::ecs::system::SystemId;
@@ -6,6 +7,7 @@ use bevy::math::bounding::{Aabb3d, IntersectsVolume};
=use bevy::prelude::*;
=use bevy_rts_camera::Ground;
=use itertools::Itertools;
+use serde::{Deserialize, Serialize};
=use std::f32::consts::FRAC_PI_2;
=
=pub struct GroundPlugin;
@@ -36,15 +38,14 @@ fn register_ground_systems(world: &mut World) {
=
=fn remove_obstacles(
= In(aabb): In<Aabb3d>,
- obstacles: Query<(Entity, &GlobalTransform), With<Tree>>,
- mut commands: Commands,
+ mut obstacles: Query<(&mut Visibility, &GlobalTransform), With<Tree>>,
=) {
= debug!("Removing obstacles in {aabb:#?}");
=
= let mut left = 0;
= let mut removed = 0;
=
- for (entity, transform) in obstacles.iter() {
+ for (mut visibility, transform) in obstacles.iter_mut() {
= let obstacle_bound = Aabb3d::new(
= transform.translation(),
= Vec3 {
@@ -54,9 +55,9 @@ fn remove_obstacles(
= },
= );
= if aabb.intersects(&obstacle_bound) {
- debug!("Intersecting {obstacle_bound:#?} and {aabb:#?}. Removing obstacle #{entity:?}");
+ debug!("Intersecting {obstacle_bound:#?} and {aabb:#?}. Removing the obstacle.");
= removed += 1;
- commands.entity(entity).despawn_recursive();
+ *visibility = Visibility::Hidden;
= } else {
= left += 1;
= }
@@ -174,3 +175,19 @@ fn setup_ground(
=
=#[derive(Component, Debug)]
=struct SnowGlobe;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GroundSnapshot;
+
+impl Snapshot for GroundSnapshot {
+ fn capture(world: &mut World) -> Self {
+ Self
+ }
+
+ fn restore(&self, world: &mut World) {
+ let mut obstacles = world.query_filtered::<&mut Visibility, With<Tree>>();
+ for mut visibility in obstacles.iter_mut(world) {
+ *visibility = Visibility::Visible
+ }
+ }
+}index 026bc10..fddff06 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -3,6 +3,7 @@ use crate::date::{Date, DateSnapshot, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
+use crate::ground::GroundSnapshot;
=use crate::major_state::{MajorState, ReadyPredicates};
=use crate::population::{Died, Immigrated, MovedIn, MovedOut, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
@@ -188,6 +189,7 @@ pub struct MainSnapshot {
= date: Date,
=
= // TODO: Replace all below with a single Vec<Box<dyn Snapshot>>,
+ ground: GroundSnapshot,
= buildings: BuildingsSnapshot,
= districts: DistrictsSnapshot,
= roads: RoadsSnapshot,
@@ -198,6 +200,7 @@ pub struct MainSnapshot {
=pub fn take_snapshot(world: &mut World) {
= // TODO: DRY on take_snapshot. Maybe a macro?
= let date = DateSnapshot::capture(world);
+ let ground = GroundSnapshot::capture(world);
= let roads = RoadsSnapshot::capture(world);
= let districts = DistrictsSnapshot::capture(world);
= let buildings = BuildingsSnapshot::capture(world);
@@ -207,6 +210,7 @@ pub fn take_snapshot(world: &mut World) {
=
= let snapshot = MainSnapshot {
= date,
+ ground,
= buildings,
= districts,
= roads,
@@ -230,6 +234,7 @@ fn rollback(In(snapshot): In<MainSnapshot>, world: &mut World) {
=
= // TODO: DRY on rollback. Maybe a macro?
= snapshot.date.restore(world);
+ snapshot.ground.restore(world);
= snapshot.roads.restore(world);
= snapshot.districts.restore(world);
= snapshot.buildings.restore(world);