Week 11 of 2024

Development log of Otterhide

35 items
  1. Fixed roads in roads.blend to 4m. Changed roads sidewalk to light grey. Fixed neighbourhood alignment and renamed the file.
  2. Exported models and replaced usage for new district
  3. Two initial roads intersect at the center
  4. Implement roads snapshots and rollback
  5. Clean up the roads module
  6. Apply all Clippy fixes
  7. WIP: Parcels and building classes
  8. Fix wrong transform of new buildings
  9. Enable World Inspector plugin to help in debugging
  10. Fix the parent without GlobalTransform warnings
  11. Make some systems oblivious to new month
  12. Remove some unused imports and parameters
  13. Update the buildings.glb export
  14. Give names to district and road entities
  15. Use different building models from buildings.blend
  16. Upgrade Nix dependencies
  17. Drop unused constant
  18. Use iterators more concisely
  19. Make timescale into a resource
  20. Drop another unused constant
  21. Debugging asset crashing the app (wip).
  22. reverted stash commit - buildings with models in parcels.
  23. Re-enable snapshots and rollback for buildings
  24. Fix program crashing on some building data
  25. Re-adding districts and new buildings.
  26. Increased the amount of building orders to 5 per class.
  27. Rename a variable
  28. Make the logs less verbose
  29. WIP: Intelligent districts placement
  30. Refactor District type to store width and length
  31. Switch names of the width and length district constants
  32. All sides of an intersections will be considered
  33. Fix districts not getting established
  34. Implement new district rotation, randomness
  35. Prevent districts from going beyond the landmass

Fixed roads in roads.blend to 4m. Changed roads sidewalk to light grey. Fixed neighbourhood alignment and renamed the file.

On by pedrolinux

similarity index 67%
rename from art/neighbourhood_300x180_A.blend
rename to art/districts.blend
index 60827e8..35c0b25 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/districts.blend differ
index 4937b95..7b1abb6 100644
Binary files a/art/roads.blend and b/art/roads.blend differ

Exported models and replaced usage for new district

On by pedrolinux

index 5bf4181..2d4e0f3 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differ
index 35c0b25..2067fe1 100644
Binary files a/art/districts.blend and b/art/districts.blend differ
index 7b1abb6..8c9141e 100644
Binary files a/art/roads.blend and b/art/roads.blend differ
new file mode 100644
index 0000000..57fe1bc
Binary files /dev/null and b/assets/district_180x300_a.glb differ
index ecc8e3f..c14c76f 100644
Binary files a/assets/roads.glb and b/assets/roads.glb differ
index 943dcd9..2ee674a 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -201,7 +201,7 @@ fn setup_spawn_district(world: &mut World) {
=    world.insert_resource(SpawnDistrict(system));
=}
=
-const MODEL_PATH: &str = "district_180x300_b.glb#Scene0";
+const MODEL_PATH: &str = "district_180x300_a.glb#Scene0";
=
=fn spawn_district(
=    In(district): In<District>,

Two initial roads intersect at the center

On by Tad Lispy

I refactored road construction system a lot, mainly by extracting and abstracting logic from District::border_roads (now renamed to plan_border_roads). There is a new helper type RoadPlan, that is similar to existing Roads, but doesn't hold references to Bevy entities, just data about roads. Currently it can be applied to construct the entities and update Roads, but soon I will also use it to take snapshots of existing roads for time traveling.

The new one shot system implement_road_plan is combined with the existing construct_road_section (renamed from add_section) into a single resource RoadConstructors. This resource is used in both roads and in districts modules, and is intended to be the main interface for modifying roads in the simulation.

There is also a bunch of new helper methods for District, Coordinates, Latitude and Longitude.

index c720ffe..39f24b9 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -21,9 +21,19 @@ pub enum Direction {
=    South = 0b0100,
=    West = 0b1000,
=}
+impl Direction {
+    pub(crate) fn opposite(&self) -> Direction {
+        match self {
+            Direction::North => Direction::South,
+            Direction::East => Direction::West,
+            Direction::South => Direction::North,
+            Direction::West => Direction::East,
+        }
+    }
+}
=
=impl Coordinates {
-    pub fn shift(&mut self, distance: i32, direction: Direction) -> &mut Self {
+    pub fn shift(&mut self, distance: i32, direction: &Direction) -> &mut Self {
=        match direction {
=            Direction::North => self.1 -= distance,
=            Direction::East => self.0 += distance,
@@ -55,6 +65,8 @@ impl From<&Coordinates> for Vec3 {
=    }
=}
=
+// TODO: DRY on Latitude and Longitude. Maybe create a derivable trait Coordinate?
+
=impl AddAssign<i32> for Latitude {
=    fn add_assign(&mut self, rhs: i32) {
=        self.0 += rhs;
@@ -80,21 +92,39 @@ impl SubAssign<i32> for Longitude {
=}
=
=impl Latitude {
+    pub fn new(value: i32) -> Self {
+        Self(value)
+    }
+
=    pub fn range(&self, other: &Self) -> Vec<Self> {
=        let start = self.min(other).0;
=        let end = self.max(other).0;
=        let values = start..=end;
=        values.map(Self::from).collect()
=    }
+
+    /// Get a distance between two latitudes
+    pub fn distance(&self, other: &Self) -> u32 {
+        self.0.abs_diff(other.0)
+    }
=}
=
=impl Longitude {
+    pub fn new(value: i32) -> Self {
+        Self(value)
+    }
+
=    pub fn range(&self, other: &Self) -> Vec<Self> {
=        let start = self.min(other).0;
=        let end = self.max(other).0;
=        let values = start..=end;
=        values.map(Self::from).collect()
=    }
+
+    /// Get a distance between two longitudes
+    pub fn distance(&self, other: &Self) -> u32 {
+        self.0.abs_diff(other.0)
+    }
=}
=
=impl Display for Coordinates {
@@ -117,7 +147,7 @@ mod coordinates_tests {
=    #[test]
=    fn shifting() {
=        let mut a = Coordinates::default();
-        a.shift(4, Direction::East).shift(5, Direction::South);
+        a.shift(4, &Direction::East).shift(5, &Direction::South);
=        assert_eq!(a, Coordinates::new(Longitude::from(4), Latitude::from(5)));
=    }
=}
index a33c72b..187da47 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,11 +1,10 @@
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
=use crate::history::TimeTravelRequest;
-use crate::roads::{RoadSection, SpawnRoadSection};
+use crate::roads::{RoadConstructors, RoadPlan};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
-use bevy::utils::HashSet;
=use std::borrow::Borrow;
=
=pub struct DistrictsPlugin;
@@ -37,85 +36,17 @@ pub struct District {
=}
=
=impl District {
-    pub fn border_roads(&self) -> HashSet<(Coordinates, RoadSection)> {
-        let westernmost = self.west();
-        let northernmost = self.north();
-        let easternmost = self.east();
-        let southernmost = self.south();
-
-        let longitudinal_range = westernmost.range(&easternmost);
-        let latitudinal_range = northernmost.range(&southernmost);
-
-        // TODO: Extract into a helper function in roads module
-        let northern_border = longitudinal_range.iter().map(|longitude| {
-            let section = if longitude == &westernmost {
-                // Beginning of the road
-                RoadSection::default().add_direction(&Direction::East)
-            } else if longitude == &easternmost {
-                // End of the road
-                RoadSection::default().add_direction(&Direction::West)
-            } else {
-                // Mid section
-                RoadSection::default()
-                    .add_direction(&Direction::East)
-                    .add_direction(&Direction::West)
-            };
-            (Coordinates::new(*longitude, northernmost), section)
-        });
-
-        let southern_border = longitudinal_range.iter().map(|longitude| {
-            let section = if *longitude == westernmost {
-                // Beginning of the road
-                RoadSection::default().add_direction(&Direction::East)
-            } else if *longitude == easternmost {
-                // End of the road
-                RoadSection::default().add_direction(&Direction::West)
-            } else {
-                // Mid section
-                RoadSection::default()
-                    .add_direction(&Direction::East)
-                    .add_direction(&Direction::West)
-            };
-            (Coordinates::new(*longitude, southernmost), section)
-        });
-        let eastern_border = latitudinal_range.iter().map(|latitude| {
-            let section = if *latitude == northernmost {
-                // Beginning of the road
-                RoadSection::default().add_direction(&Direction::South)
-            } else if *latitude == southernmost {
-                // End of the road
-                RoadSection::default().add_direction(&Direction::North)
-            } else {
-                // Mid section
-                RoadSection::default()
-                    .add_direction(&Direction::South)
-                    .add_direction(&Direction::North)
-            };
-            (Coordinates::new(easternmost, *latitude), section)
-        });
-        let western_border = latitudinal_range.iter().map(|latitude| {
-            let section = if *latitude == northernmost {
-                // Beginning of the road
-                RoadSection::default().add_direction(&Direction::South)
-            } else if *latitude == southernmost {
-                // End of the road
-                RoadSection::default().add_direction(&Direction::North)
-            } else {
-                // Mid section
-                RoadSection::default()
-                    .add_direction(&Direction::South)
-                    .add_direction(&Direction::North)
-            };
-            (Coordinates::new(westernmost, *latitude), section)
-        });
-
-        let borders = northern_border
-            .chain(southern_border)
-            .chain(eastern_border)
-            .chain(western_border);
-        HashSet::from_iter(borders)
+    pub fn plan_border_roads(&self) -> RoadPlan {
+        RoadPlan::default()
+            .add_road(&self.north_west(), &Direction::East, self.length())
+            .add_road(&self.north_east(), &Direction::South, self.width())
+            .add_road(&self.south_east(), &Direction::West, self.length())
+            .add_road(&self.south_west(), &Direction::North, self.width())
+            .to_owned()
=    }
=
+    // Edges
+
=    fn south(&self) -> Latitude {
=        self.origin.latitude().max(self.extent.latitude())
=    }
@@ -131,6 +62,36 @@ impl District {
=    fn west(&self) -> Longitude {
=        self.origin.longitude().min(self.extent.longitude())
=    }
+
+    // Corners
+
+    fn north_east(&self) -> Coordinates {
+        Coordinates::new(self.east(), self.north())
+    }
+
+    fn north_west(&self) -> Coordinates {
+        Coordinates::new(self.west(), self.north())
+    }
+
+    fn south_east(&self) -> Coordinates {
+        Coordinates::new(self.east(), self.south())
+    }
+
+    fn south_west(&self) -> Coordinates {
+        Coordinates::new(self.west(), self.south())
+    }
+
+    // Dimensions
+
+    /// Dimension from the east to the west edge (along the longitude)
+    fn length(&self) -> u32 {
+        self.east().distance(&self.west())
+    }
+
+    /// Dimension from the north to the south edge (along the latitude)
+    fn width(&self) -> u32 {
+        self.north().distance(&self.south())
+    }
=}
=
=fn establish_new_districts(
@@ -154,8 +115,8 @@ fn establish_new_districts(
=    let origin = Coordinates::new(Longitude::from(x), Latitude::from(y));
=    let extent = origin
=        .clone()
-        .shift(18, Direction::East)
-        .shift(30, Direction::South)
+        .shift(18, &Direction::East)
+        .shift(30, &Direction::South)
=        .to_owned();
=
=    let district = District { origin, extent };
@@ -207,7 +168,7 @@ fn spawn_district(
=    In(district): In<District>,
=    assets: ResMut<AssetServer>,
=    mut commands: Commands,
-    spawn_road_section: Res<SpawnRoadSection>,
+    road_constructors: Res<RoadConstructors>,
=) {
=    info!("Spawning a district: {district:?}");
=
@@ -220,17 +181,47 @@ fn spawn_district(
=        })
=        .insert(district);
=
-    for section in district.border_roads() {
-        commands.run_system_with_input(spawn_road_section.0, section.to_owned());
-    }
+    commands.run_system_with_input(
+        road_constructors.implement_road_plan,
+        district.plan_border_roads(),
+    );
=}
=
=#[cfg(test)]
=mod district_tests {
-    use std::ops::Not;
-
=    use super::*;
=    use bevy::math::Rect;
+    use std::ops::Not;
+
+    #[test]
+    fn measurements_test() {
+        let origin = Coordinates::new(Longitude::new(-3), Latitude::new(-2));
+        let extent = Coordinates::new(Longitude::new(10), Latitude::new(5));
+        let district = District { origin, extent };
+
+        assert_eq!(district.east(), Longitude::new(10));
+        assert_eq!(district.west(), Longitude::new(-3));
+        assert_eq!(district.north(), Latitude::new(-2));
+        assert_eq!(district.south(), Latitude::new(5));
+        assert_eq!(district.length(), 13);
+        assert_eq!(district.width(), 7);
+        assert_eq!(
+            district.north_east(),
+            Coordinates::new(Longitude::new(10), Latitude::new(-2))
+        );
+        assert_eq!(
+            district.north_west(),
+            Coordinates::new(Longitude::new(-3), Latitude::new(-2))
+        );
+        assert_eq!(
+            district.south_east(),
+            Coordinates::new(Longitude::new(10), Latitude::new(5))
+        );
+        assert_eq!(
+            district.south_west(),
+            Coordinates::new(Longitude::new(-3), Latitude::new(5))
+        );
+    }
=
=    #[test]
=    fn overlapping() {
index c258bac..c533825 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,11 +1,13 @@
=use crate::coordinates::{Coordinates, Direction};
=use crate::history::TimeTravelRequest;
-use crate::PreloadedAssets;
+use crate::{GameState, PreloadedAssets, Settings};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
+use bevy::utils::hashbrown::HashSet;
=use bevy::utils::HashMap;
=use core::fmt;
+use derive_more::{From, Into};
=use std::borrow::Borrow;
=use std::fmt::Display;
=use std::ops::Not;
@@ -16,10 +18,9 @@ impl Plugin for RoadsPlugin {
=    fn build(&self, app: &mut App) {
=        app.init_resource::<Roads>()
=            .add_systems(Startup, setup)
-            .add_systems(Startup, setup_spawn_road_section)
+            .add_systems(Startup, setup_road_constructors)
=            .add_systems(Update, rollback)
-        // .add_systems(OnEnter(GameState::Simulate), lay_initial_roads)
-        ;
+            .add_systems(OnEnter(GameState::Simulate), lay_initial_roads);
=    }
=}
=
@@ -34,64 +35,49 @@ fn setup(assets: Res<AssetServer>, mut commands: Commands, mut preloaded: ResMut
=    commands.insert_resource(RoadAssets(handle));
=}
=
-fn lay_initial_roads(mut commands: Commands, spawn_road_section: Res<SpawnRoadSection>) {
+fn lay_initial_roads(
+    mut commands: Commands,
+    settings: Res<Settings>,
+    constructors: Res<RoadConstructors>,
+) {
=    info!("Laying initial roads.");
=
-    // Longitudinal roads
-    // let x_min = -12;
-    // let x_max = 12;
-    // let y_min = -1;
-    // let y_max = 2;
-    // for x in x_min..=x_max {
-    //     for y in y_min..=y_max {
-    //         let new_section = if x == x_min {
-    //             RoadSection::default().add_direction(&Direction::East)
-    //         } else if x == x_max {
-    //             RoadSection::default().add_direction(&Direction::West)
-    //         } else {
-    //             RoadSection::default()
-    //                 .add_direction(&Direction::East)
-    //                 .add_direction(&Direction::West)
-    //         };
-
-    //         commands
-    //             .run_system_with_input(spawn_road_section.0, (Coordinates(x, y * 10), new_section));
-    //     }
-    // }
-
-    // // Latitudinal roads
-    // let x_min = -3;
-    // let x_max = 2;
-    // let y_min = -10;
-    // let y_max = 10;
-    // for x in x_min..=x_max {
-    //     for y in y_min..=y_max {
-    //         let new_section = if y == y_min {
-    //             RoadSection::default().add_direction(&Direction::South)
-    //         } else if y == y_max {
-    //             RoadSection::default().add_direction(&Direction::North)
-    //         } else {
-    //             RoadSection::default()
-    //                 .add_direction(&Direction::South)
-    //                 .add_direction(&Direction::North)
-    //         };
-    //         commands
-    //             .run_system_with_input(spawn_road_section.0, (Coordinates(x * 6, y), new_section));
-    //     }
-    // }
+    let center = Coordinates::default();
+    let length = settings.land_radius as u32 / 10;
+    let mut plan = RoadPlan::default();
+
+    for direction in [
+        Direction::North,
+        Direction::East,
+        Direction::South,
+        Direction::West,
+    ] {
+        plan.add_road(&center, &direction, length);
+    }
+
+    commands.run_system_with_input(constructors.implement_road_plan, plan);
=}
=
=// Spawn district is a one-shot system attached to a resource. It does what it says on the tin.
=
+/// A collection of one-shot systems that create new roads
=#[derive(Resource, Debug)]
-pub struct SpawnRoadSection(pub SystemId<(Coordinates, RoadSection)>);
+pub struct RoadConstructors {
+    pub construct_road_section: SystemId<(Coordinates, RoadSection)>,
+    pub implement_road_plan: SystemId<RoadPlan>,
+}
+
+fn setup_road_constructors(world: &mut World) {
+    let construct_road_section = world.register_system(construct_road_section);
+    let implement_road_plan = world.register_system(implement_road_plan);
=
-fn setup_spawn_road_section(world: &mut World) {
-    let system = world.register_system(add_section);
-    world.insert_resource(SpawnRoadSection(system));
+    world.insert_resource(RoadConstructors {
+        construct_road_section,
+        implement_road_plan,
+    });
=}
=
-fn add_section(
+fn construct_road_section(
=    In((coordinates, new_section)): In<(Coordinates, RoadSection)>,
=    mut commands: Commands,
=    road_assets: Res<RoadAssets>,
@@ -141,6 +127,16 @@ fn add_section(
=    };
=}
=
+fn implement_road_plan(
+    In(plan): In<RoadPlan>,
+    mut commands: Commands,
+    constructors: Res<RoadConstructors>,
+) {
+    for section in plan.sections {
+        commands.run_system_with_input(constructors.construct_road_section, section)
+    }
+}
+
=fn rollback(
=    mut requests: EventReader<TimeTravelRequest>,
=    sections: Query<Entity, With<RoadSection>>,
@@ -155,6 +151,44 @@ fn rollback(
=        // TODO: Re-lay initial roads
=    }
=}
+
+/// Represents road sections with their positions, but without reference to entities in the world.
+///
+/// It's kind of a plan that shows roads that are supposed to be constructed or
+/// a snapshot of existing roads.
+#[derive(Debug, From, Into, Clone, Default)]
+pub struct RoadPlan {
+    // It holds a HashSet instead of a HashMap, because two road sections can
+    // occupy same coordinate, e.g. one N-S and the other E-W. Once constructed
+    // (using implement_road_plan for example), they will be combined into an
+    // intersection.
+    sections: HashSet<(Coordinates, RoadSection)>,
+}
+
+impl RoadPlan {
+    // Plan a straight road
+    pub fn add_road(
+        &mut self,
+        from: &Coordinates,
+        direction: &Direction,
+        lenght: u32,
+    ) -> &mut Self {
+        for distance in 0..=lenght {
+            let coordinates = from.clone().shift(distance as i32, &direction).to_owned();
+            let section = if distance == 0 {
+                RoadSection::start(direction)
+            } else if distance == lenght {
+                RoadSection::end(direction)
+            } else {
+                RoadSection::middle(direction)
+            };
+            self.sections.insert((coordinates, section));
+        }
+        self
+    }
+}
+
+/// This holds references to actual road entities
=#[derive(Resource, Default)]
=pub struct Roads {
=    sections: HashMap<Coordinates, Entity>,
@@ -196,6 +230,23 @@ impl RoadSection {
=    pub fn scene_index(&self) -> usize {
=        self.directions.into()
=    }
+
+    // Beginning of the road
+    fn start(direction: &Direction) -> Self {
+        Self::default().add_direction(direction)
+    }
+
+    // Mid-section
+    fn middle(direction: &Direction) -> Self {
+        Self::default()
+            .add_direction(direction)
+            .add_direction(&direction.opposite())
+    }
+
+    // End of the road
+    fn end(direction: &Direction) -> Self {
+        Self::default().add_direction(&direction.opposite())
+    }
=}
=
=#[derive(Copy, Clone, PartialEq, Eq, Hash)]

Implement roads snapshots and rollback

On by Tad Lispy

Also overhaul the whole snapshots and rollback system.

It is no longer events based. Now each module exposes two one-shot systems: take_snapshot and rollback. The history module calls those systems appropriately. Each module has it's own Snapshot type, that only holds data relevant to this module.

This allows for some decoupling, where each module (roads, districts, date, etc.) can manage their own snapshot and rollback logic. The drawback is that there is quite a lot of boilerplate. I think it can be dealt with using some macros.

index beb220d..42b67cd 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,6 +1,4 @@
=use crate::date::NewMonth;
-use crate::history::TimeTravelRequest;
-use crate::GameState;
=use bevy::prelude::*;
=
=pub struct BuildingsPlugin;
@@ -13,8 +11,7 @@ impl Plugin for BuildingsPlugin {
=            //     Update,
=            //     order_construction.run_if(in_state(GameState::Simulate)),
=            // )
-            .add_systems(Update, spawn_buildings)
-            .add_systems(Update, rollback);
+            .add_systems(Update, spawn_buildings);
=    }
=}
=
@@ -76,28 +73,28 @@ fn spawn_buildings(
=    }
=}
=
-fn rollback(
-    mut requests: EventReader<TimeTravelRequest>,
-    buildings: Query<Entity, With<Building>>,
-    assets: ResMut<AssetServer>,
-    mut commands: Commands,
-) {
-    for TimeTravelRequest(snapshot) in requests.read() {
-        for entity in buildings.iter() {
-            commands.entity(entity).despawn_recursive();
-        }
-
-        for transform in snapshot.clone().buildings {
-            // TODO: DRY with spawn_buildings
-            let model = assets.load(MODEL_PATH);
-
-            commands
-                .spawn(SceneBundle {
-                    scene: model,
-                    transform: transform.clone(),
-                    ..default()
-                })
-                .insert(Building);
-        }
-    }
-}
+// fn rollback(
+//     mut requests: EventReader<TimeTravelRequest>,
+//     buildings: Query<Entity, With<Building>>,
+//     assets: ResMut<AssetServer>,
+//     mut commands: Commands,
+// ) {
+//     for TimeTravelRequest(snapshot) in requests.read() {
+//         for entity in buildings.iter() {
+//             commands.entity(entity).despawn_recursive();
+//         }
+
+//         for transform in snapshot.clone().buildings {
+//             // TODO: DRY with spawn_buildings
+//             let model = assets.load(MODEL_PATH);
+
+//             commands
+//                 .spawn(SceneBundle {
+//                     scene: model,
+//                     transform: transform.clone(),
+//                     ..default()
+//                 })
+//                 .insert(Building);
+//         }
+//     }
+// }
index 2bfbca3..3ceb389 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,6 +1,6 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
-use crate::history::TimeTravelRequest;
=use crate::{GameState, BEGINNING, DURATION};
+use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=
=/// Day-months per one second of real time during exploration
@@ -15,8 +15,8 @@ impl Plugin for DatePlugin {
=        app.insert_resource(Date(DayMonth::new(BEGINNING)))
=            .add_event::<NewMonth>()
=            .add_systems(Startup, setup_date_display)
-            .add_systems(Update, (advance_date, update_date_display))
-            .add_systems(Update, rollback);
+            .add_systems(Startup, register_date_systems)
+            .add_systems(Update, (advance_date, update_date_display));
=    }
=}
=
@@ -85,8 +85,30 @@ fn update_date_display(mut display: Query<&mut Text, With<DateDisplay>>, date: R
=    display.sections[0].value = date.0.to_string();
=}
=
-fn rollback(mut requests: EventReader<TimeTravelRequest>, mut date: ResMut<Date>) {
-    for TimeTravelRequest(snapshot) in requests.read() {
-        date.0 = snapshot.date;
-    }
+pub type Snapshot = DayMonth;
+
+pub fn take_snapshot(date: Res<Date>) -> Snapshot {
+    date.0
+}
+
+fn rollback(In(snapshot): In<Snapshot>, mut date: ResMut<Date>) {
+    date.0 = snapshot
+}
+
+// Systems exposed to other systems
+
+#[derive(Resource, Debug)]
+pub struct DateSystems {
+    pub take_snapshot: SystemId<(), Snapshot>,
+    pub rollback: SystemId<Snapshot>,
+}
+
+fn register_date_systems(world: &mut World) {
+    let take_snapshot = world.register_system(take_snapshot);
+    let rollback = world.register_system(rollback);
+
+    world.insert_resource(DateSystems {
+        take_snapshot,
+        rollback,
+    });
=}
index 187da47..23fa21d 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,7 +1,6 @@
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
-use crate::history::TimeTravelRequest;
-use crate::roads::{RoadConstructors, RoadPlan};
+use crate::roads::{RoadPlan, RoadsSystems};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
@@ -12,13 +11,12 @@ pub struct DistrictsPlugin;
=impl Plugin for DistrictsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<NewDistrictEstablished>()
-            .add_systems(Startup, setup_spawn_district)
+            .add_systems(Startup, register_district_systems)
=            .add_systems(
=                Update,
=                establish_new_districts.run_if(in_state(GameState::Simulate)),
=            )
-            .add_systems(Update, spawn_new_districts)
-            .add_systems(Update, rollback);
+            .add_systems(Update, spawn_new_districts);
=    }
=}
=
@@ -128,47 +126,62 @@ fn establish_new_districts(
=fn spawn_new_districts(
=    mut commands: Commands,
=    mut new_districts: EventReader<NewDistrictEstablished>,
-    spawn_district: Res<SpawnDistrict>,
+    systems: Res<DistrictsSystems>,
=) {
=    for NewDistrictEstablished(district) in new_districts.read() {
-        commands.run_system_with_input(spawn_district.0, district.to_owned());
+        commands.run_system_with_input(systems.construct_district, *district);
=    }
=}
=
+pub type Snapshot = Vec<District>;
+
+pub fn take_snapshot(districts: Query<&District>) -> Snapshot {
+    districts.iter().map(|district| *district).collect()
+}
+
=fn rollback(
-    mut requests: EventReader<TimeTravelRequest>,
+    In(snapshot): In<Snapshot>,
=    districts: Query<Entity, With<District>>,
=    mut commands: Commands,
-    spawn_district: Res<SpawnDistrict>,
+    systems: Res<DistrictsSystems>,
=) {
-    for TimeTravelRequest(snapshot) in requests.read() {
-        for entity in districts.iter() {
-            commands.entity(entity).despawn_recursive();
-        }
+    for entity in districts.iter() {
+        commands.entity(entity).despawn_recursive();
+    }
=
-        for district in snapshot.clone().districts {
-            commands.run_system_with_input(spawn_district.0, district.to_owned());
-        }
+    for district in snapshot {
+        commands.run_system_with_input(systems.construct_district, district.to_owned());
=    }
=}
=
-// Spawn district is a one-shot system attached to a resource. It does what it says on the tin.
+// Systems exposed to other systems
=
=#[derive(Resource, Debug)]
-struct SpawnDistrict(SystemId<District>);
+pub struct DistrictsSystems {
+    pub take_snapshot: SystemId<(), Snapshot>,
+    pub rollback: SystemId<Snapshot>,
+    pub construct_district: SystemId<District>,
+}
+
+fn register_district_systems(world: &mut World) {
+    let take_snapshot = world.register_system(take_snapshot);
+    let rollback = world.register_system(rollback);
+    let construct_district = world.register_system(construct_district);
=
-fn setup_spawn_district(world: &mut World) {
-    let system = world.register_system(spawn_district);
-    world.insert_resource(SpawnDistrict(system));
+    world.insert_resource(DistrictsSystems {
+        take_snapshot,
+        rollback,
+        construct_district,
+    });
=}
=
=const MODEL_PATH: &str = "district_180x300_a.glb#Scene0";
=
-fn spawn_district(
+fn construct_district(
=    In(district): In<District>,
=    assets: ResMut<AssetServer>,
=    mut commands: Commands,
-    road_constructors: Res<RoadConstructors>,
+    road_constructors: Res<RoadsSystems>,
=) {
=    info!("Spawning a district: {district:?}");
=
index d9b8ac5..f8ebcae 100644
--- a/src/explore.rs
+++ b/src/explore.rs
@@ -1,5 +1,5 @@
=use crate::history::History;
-use crate::history::TimeTravelRequest;
+use crate::history::HistorySystems;
=use crate::pgsql_export;
=use crate::GameState;
=use bevy::prelude::*;
@@ -18,7 +18,8 @@ impl Plugin for ExplorePlugin {
=fn paint_ui(
=    mut contexts: EguiContexts,
=    history: Res<History>,
-    mut requests: EventWriter<TimeTravelRequest>,
+    systems: Res<HistorySystems>,
+    mut commands: Commands,
=) {
=    egui::SidePanel::right("explore_ui").show(contexts.ctx_mut(), |ui| {
=        ui.heading("Explore");
@@ -42,7 +43,7 @@ fn paint_ui(
=                let button = ui.button(format!("{month} {year}"));
=                if button.clicked() {
=                    info!("Time travel to {timestamp}");
-                    requests.send(TimeTravelRequest(snapshot.clone()));
+                    commands.run_system_with_input(systems.rollback, snapshot.clone())
=                }
=            }
=        })
index c536fad..a383713 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,12 +1,15 @@
+use crate::buildings;
=use crate::buildings::ConstructionOrder;
-use crate::buildings::{self, Building};
-use crate::date::{Date, NewMonth};
+use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
-use crate::districts::{District, NewDistrictEstablished};
+use crate::districts::{DistrictsSystems, NewDistrictEstablished};
+use crate::roads::{self, RoadsSystems};
=use crate::{districts, GameState};
+use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::HashMap;
=use std::fmt::Display;
+use std::ops::DerefMut;
=
=pub struct HistoryPlugin;
=
@@ -14,11 +17,8 @@ impl Plugin for HistoryPlugin {
=    fn build(&self, app: &mut App) {
=        app.init_resource::<History>()
=            .init_resource::<Future>()
-            .add_event::<TimeTravelRequest>()
-            .add_systems(
-                PreUpdate,
-                take_snapshot.run_if(in_state(GameState::Simulate)),
-            )
+            .add_systems(Startup, setup_history_systems)
+            .add_systems(PreUpdate, take_snapshot.run_if(on_event::<NewMonth>()))
=            .add_systems(
=                Update,
=                register_historical_events.run_if(in_state(GameState::Simulate)),
@@ -26,8 +26,7 @@ impl Plugin for HistoryPlugin {
=            .add_systems(
=                Update,
=                replay_historical_events.run_if(in_state(GameState::Explore)),
-            )
-            .add_systems(Update, time_travel.run_if(in_state(GameState::Explore)));
+            );
=    }
=}
=
@@ -98,57 +97,96 @@ fn register_historical_events(
=
=// TODO: Use hankjordan/bevy_save with a custom pipeline
=// See https://github.com/hankjordan/bevy_save/blob/6fe1d55c822ecd075db9a86d383042c9fa07c192/examples/breakout.rs#L614
+
+/// The main snapshot that binds them all
=#[derive(Clone, Debug)]
=pub struct Snapshot {
=    pub date: DayMonth,
=    pub buildings: Vec<Transform>,
-    pub districts: Vec<District>,
+    pub districts: districts::Snapshot,
+    pub roads: roads::Snapshot,
=}
=
-fn take_snapshot(
-    mut new_month: EventReader<NewMonth>,
-    date: Res<Date>,
-    mut history: ResMut<History>,
-    buildings: Query<&Transform, With<Building>>,
-    districts: Query<&District>,
-) {
-    if new_month.read().count() == 0 {
-        return;
-    }
+fn take_snapshot(world: &mut World) {
+    // TODO: DRY on take_snapshot. Maybe a macro?
+    let date = {
+        let system = world.resource_scope(|_, systems: Mut<DateSystems>| systems.take_snapshot);
+        world.run_system(system).unwrap()
+    };
+
+    let roads = {
+        let system = world.resource_scope(|_, systems: Mut<RoadsSystems>| systems.take_snapshot);
+        world.run_system(system).unwrap()
+    };
+
+    let districts = {
+        let system =
+            world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.take_snapshot);
+        world.run_system(system).unwrap()
+    };
+    let buildings = Vec::default();
+    // let buildings = buildings.iter().map(|transform| *transform).collect();
+
+    let timestamp: Timestamp = date.into();
=
-    let timestamp: Timestamp = date.0.into();
-    let buildings = buildings.iter().map(|transform| *transform).collect();
-    let districts = districts.iter().map(|district| *district).collect();
=    let snapshot = Snapshot {
-        date: date.0,
+        date,
=        buildings,
=        districts,
+        roads,
=    };
-    history.snapshots.insert(timestamp.clone(), snapshot);
=
-    let count = history.snapshots.len();
-    info!("Registering snapshot {count} on {timestamp}.")
+    world.resource_scope(|_, mut history: Mut<History>| {
+        history
+            .deref_mut()
+            .snapshots
+            .insert(timestamp.clone(), snapshot);
+        let count = history.snapshots.len();
+        info!("Registering snapshot {count} on {timestamp}.")
+    })
=}
=
-#[derive(Event)]
-pub struct TimeTravelRequest(pub Snapshot);
-
-fn time_travel(
-    mut requests: EventReader<TimeTravelRequest>,
-    mut game_state: ResMut<NextState<GameState>>,
-    mut future: ResMut<Future>,
-    history: Res<History>,
-) {
-    for request in requests.read() {
+fn rollback(In(snapshot): In<Snapshot>, world: &mut World) {
+    // TODO: DRY on rollback. Maybe a macro?
+    world.resource_scope(|_, mut game_state: Mut<NextState<GameState>>| {
=        game_state.set(GameState::Explore);
+    });
+
+    {
+        let system = world.resource_scope(|_, systems: Mut<DateSystems>| systems.rollback);
+        world.run_system_with_input(system, snapshot.date).unwrap();
+    }
+    {
+        let system = world.resource_scope(|_, systems: Mut<RoadsSystems>| systems.rollback);
+        world.run_system_with_input(system, snapshot.roads).unwrap();
+    }
+
+    {
+        let system = world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.rollback);
+        world
+            .run_system_with_input(system, snapshot.districts)
+            .unwrap();
+    }
=
-        future.events = history
-            .events
+    let events = world.resource_scope(|_, history: Mut<History>| history.events.clone());
+
+    world.resource_scope(|_, mut future: Mut<Future>| {
+        future.events = events
=            .clone()
=            .into_iter()
-            .filter(|event| event.date >= request.0.date)
+            .filter(|event| event.date >= snapshot.date)
=            .collect();
-    }
+    });
+}
+
+#[derive(Debug, Resource)]
+pub struct HistorySystems {
+    pub rollback: SystemId<Snapshot>,
+}
+
+fn setup_history_systems(world: &mut World) {
+    let rollback = world.register_system(rollback);
+    world.insert_resource(HistorySystems { rollback })
=}
=
=fn replay_historical_events(
index c533825..643e4b7 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,5 +1,4 @@
=use crate::coordinates::{Coordinates, Direction};
-use crate::history::TimeTravelRequest;
=use crate::{GameState, PreloadedAssets, Settings};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
@@ -17,9 +16,8 @@ pub struct RoadsPlugin;
=impl Plugin for RoadsPlugin {
=    fn build(&self, app: &mut App) {
=        app.init_resource::<Roads>()
-            .add_systems(Startup, setup)
-            .add_systems(Startup, setup_road_constructors)
-            .add_systems(Update, rollback)
+            .add_systems(Startup, setup_assets)
+            .add_systems(Startup, register_road_systems)
=            .add_systems(OnEnter(GameState::Simulate), lay_initial_roads);
=    }
=}
@@ -27,7 +25,11 @@ impl Plugin for RoadsPlugin {
=#[derive(Resource)]
=struct RoadAssets(Handle<Gltf>);
=
-fn setup(assets: Res<AssetServer>, mut commands: Commands, mut preloaded: ResMut<PreloadedAssets>) {
+fn setup_assets(
+    assets: Res<AssetServer>,
+    mut commands: Commands,
+    mut preloaded: ResMut<PreloadedAssets>,
+) {
=    const ASSET_PATH: &str = "roads.glb";
=    info!("Loading {ASSET_PATH} asset");
=    let handle = assets.load(ASSET_PATH);
@@ -38,7 +40,7 @@ fn setup(assets: Res<AssetServer>, mut commands: Commands, mut preloaded: ResMut
=fn lay_initial_roads(
=    mut commands: Commands,
=    settings: Res<Settings>,
-    constructors: Res<RoadConstructors>,
+    constructors: Res<RoadsSystems>,
=) {
=    info!("Laying initial roads.");
=
@@ -62,16 +64,23 @@ fn lay_initial_roads(
=
=/// A collection of one-shot systems that create new roads
=#[derive(Resource, Debug)]
-pub struct RoadConstructors {
+pub struct RoadsSystems {
+    pub take_snapshot: SystemId<(), Snapshot>,
+    pub rollback: SystemId<Snapshot>,
=    pub construct_road_section: SystemId<(Coordinates, RoadSection)>,
=    pub implement_road_plan: SystemId<RoadPlan>,
=}
=
-fn setup_road_constructors(world: &mut World) {
+fn register_road_systems(world: &mut World) {
+    let take_snapshot = world.register_system(take_snapshot);
+    let rollback = world.register_system(rollback);
=    let construct_road_section = world.register_system(construct_road_section);
=    let implement_road_plan = world.register_system(implement_road_plan);
=
-    world.insert_resource(RoadConstructors {
+    info!("Setting up roads systems");
+    world.insert_resource(RoadsSystems {
+        take_snapshot,
+        rollback,
=        construct_road_section,
=        implement_road_plan,
=    });
@@ -85,8 +94,6 @@ fn construct_road_section(
=    mut roads: ResMut<Roads>,
=    mut sections: Query<&RoadSection>,
=) {
-    info!("Adding a section at {coordinates:?} maybe?");
-
=    if let Some(gltf) = assets.get(&road_assets.0) {
=        match roads.sections.get(&coordinates) {
=            None => {
@@ -98,12 +105,12 @@ fn construct_road_section(
=                    })
=                    .insert(new_section)
=                    .id();
-                info!("Laying a new road section {new_section:?} at {coordinates:?} ({entity:?})");
+                debug!("Laying a new road section {new_section:?} at {coordinates:?} ({entity:?})");
=
=                roads.sections.insert(coordinates, entity);
=            }
=            Some(entity) => {
-                info!(
+                debug!(
=                        "Combining road section at {coordinates:?} with the new section {new_section:?} with {entity:?}"
=                    );
=                let old_section = sections.get_mut(*entity).unwrap();
@@ -119,7 +126,7 @@ fn construct_road_section(
=                    })
=                    .insert(section)
=                    .id();
-                info!("Combined road section {section:?} at {coordinates:?} ({entity:?})");
+                debug!("Combined road section {section:?} at {coordinates:?} ({entity:?})");
=
=                roads.sections.insert(coordinates, entity);
=            }
@@ -130,26 +137,41 @@ fn construct_road_section(
=fn implement_road_plan(
=    In(plan): In<RoadPlan>,
=    mut commands: Commands,
-    constructors: Res<RoadConstructors>,
+    constructors: Res<RoadsSystems>,
=) {
=    for section in plan.sections {
=        commands.run_system_with_input(constructors.construct_road_section, section)
=    }
=}
=
+// TODO: Consider wrapping the code below in roads::snapshots module
+
+pub type Snapshot = RoadPlan;
+
+pub fn take_snapshot(roads: Res<Roads>, sections: Query<&RoadSection>) -> Snapshot {
+    // let roads = world.resource::<Roads>();
+    let mut plan = RoadPlan::default();
+
+    for (coordinates, entity) in roads.sections.clone() {
+        let section = sections.get(entity).unwrap();
+        plan.sections.insert((coordinates, *section));
+    }
+
+    plan
+}
+
=fn rollback(
-    mut requests: EventReader<TimeTravelRequest>,
+    In(snapshot): In<Snapshot>,
=    sections: Query<Entity, With<RoadSection>>,
=    mut roads: ResMut<Roads>,
=    mut commands: Commands,
+    systems: Res<RoadsSystems>,
=) {
-    for TimeTravelRequest(snapshot) in requests.read() {
-        for entity in sections.iter() {
-            commands.entity(entity).despawn_recursive();
-        }
-        roads.sections.clear();
-        // TODO: Re-lay initial roads
+    for entity in sections.iter() {
+        commands.entity(entity).despawn_recursive();
=    }
+    roads.sections.clear();
+    commands.run_system_with_input(systems.implement_road_plan, snapshot);
=}
=
=/// Represents road sections with their positions, but without reference to entities in the world.

Clean up the roads module

On by Tad Lispy

There was a lot of dead code, including everything related to Sides and Quadrants (supposed to serve an idea I now abandoned). Since after removal of sides, RoadSection struct would hold only one field (Directions), I merged the Directions type with RoadSection. Also fixed whatever Clippy was complaining about in roads.

index 643e4b7..f8b5e88 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -9,7 +9,6 @@ use core::fmt;
=use derive_more::{From, Into};
=use std::borrow::Borrow;
=use std::fmt::Display;
-use std::ops::Not;
=
=pub struct RoadsPlugin;
=
@@ -113,10 +112,10 @@ fn construct_road_section(
=                debug!(
=                        "Combining road section at {coordinates:?} with the new section {new_section:?} with {entity:?}"
=                    );
-                let old_section = sections.get_mut(*entity).unwrap();
-                let section = old_section.combine(&new_section).unwrap();
+                let mut old_section = *sections.get_mut(*entity).unwrap();
+                let section = old_section.combine(&new_section).to_owned();
=
-                // re-spawn new entuty
+                // re-spawn new entity
=                commands.entity(*entity).despawn_recursive();
=                let entity = commands
=                    .spawn(SceneBundle {
@@ -196,7 +195,7 @@ impl RoadPlan {
=        lenght: u32,
=    ) -> &mut Self {
=        for distance in 0..=lenght {
-            let coordinates = from.clone().shift(distance as i32, &direction).to_owned();
+            let coordinates = from.clone().shift(distance as i32, direction).to_owned();
=            let section = if distance == 0 {
=                RoadSection::start(direction)
=            } else if distance == lenght {
@@ -216,143 +215,73 @@ pub struct Roads {
=    sections: HashMap<Coordinates, Entity>,
=}
=
-#[derive(Debug, Clone, Copy, Default, PartialEq, Component, Hash, Eq)]
-pub struct RoadSection {
-    directions: Directions,
-    sides: Sides,
-}
+#[derive(Clone, Copy, Default, PartialEq, Component, Hash, Eq)]
+pub struct RoadSection(u8);
=
=impl RoadSection {
-    pub fn can_combine(&self, other: &Self) -> bool {
-        self.sides.overlaps(&other.sides).not()
-    }
-
-    pub fn combine(self, other: &Self) -> Option<Self> {
-        if self.can_combine(other) {
-            let directions = self.directions.combine(&other.directions);
-            let sides = self.sides.combine(&other.sides);
-            Some(Self { directions, sides })
-        } else {
-            None
-        }
-    }
-
-    pub fn add_direction(mut self, direction: &Direction) -> Self {
-        let new_direction = Directions::from(direction.clone());
-        self.directions = self.directions.combine(&new_direction);
+    fn combine(&mut self, other: &Self) -> &mut Self {
+        self.0 |= other.0;
=        self
=    }
=
-    pub fn mark_neighbouring_district(mut self, quadrant: &Quadrant) -> Self {
-        let side = Sides::from(quadrant.clone());
-        self.sides = self.sides.combine(&side);
+    pub fn add_direction(&mut self, direction: &Direction) -> &mut Self {
+        self.0 |= *direction as u8;
=        self
=    }
=
=    pub fn scene_index(&self) -> usize {
-        self.directions.into()
+        self.0 as usize
=    }
=
=    // Beginning of the road
=    fn start(direction: &Direction) -> Self {
-        Self::default().add_direction(direction)
+        *Self::default().add_direction(direction)
=    }
=
=    // Mid-section
=    fn middle(direction: &Direction) -> Self {
-        Self::default()
+        *Self::default()
=            .add_direction(direction)
=            .add_direction(&direction.opposite())
=    }
=
=    // End of the road
=    fn end(direction: &Direction) -> Self {
-        Self::default().add_direction(&direction.opposite())
+        *Self::default().add_direction(&direction.opposite())
=    }
=}
=
-#[derive(Copy, Clone, PartialEq, Eq, Hash)]
-pub enum Quadrant {
-    NorthEast = 0b0001,
-    EastSouth = 0b0010,
-    SouthWest = 0b0100,
-    WestNorth = 0b1000,
-}
-
-#[derive(PartialEq, Eq, Clone, Copy, Hash)]
-struct Directions(u8);
-
-impl From<Direction> for Directions {
+impl From<Direction> for RoadSection {
=    fn from(direction: Direction) -> Self {
=        Self(direction as u8)
=    }
=}
=
-impl Directions {
-    fn combine(mut self, other: &Self) -> Self {
-        self.0 |= other.0;
-        self
-    }
-
-    const NONE: Self = Self(0b0000);
-
-    // One direction (dead end)
-    const N: Self = Self(0b0001);
-    const E: Self = Self(0b0010);
-    const S: Self = Self(0b0100);
-    const W: Self = Self(0b1000);
-
-    // Straight road
-    const NS: Self = Self(0b0101);
-    const EW: Self = Self(0b1010);
-
-    // Corner
-    const NE: Self = Self(0b0011);
-    const ES: Self = Self(0b0110);
-    const NW: Self = Self(0b1001);
-    const SW: Self = Self(0b1100);
-
-    // T junction
-    const NSE: Self = Self(0b0111);
-    const EWN: Self = Self(0b1011);
-    const NSW: Self = Self(0b1101);
-    const EWS: Self = Self(0b1110);
-
-    // Cross roads
-    const X: Self = Self(0b1111);
-}
-
-impl Default for Directions {
-    fn default() -> Self {
-        Self::NONE
-    }
-}
-
-impl From<u8> for Directions {
+impl From<u8> for RoadSection {
=    fn from(value: u8) -> Self {
=        Self(value & 0b1111)
=    }
=}
=
-impl From<Directions> for u8 {
-    fn from(value: Directions) -> Self {
+impl From<RoadSection> for u8 {
+    fn from(value: RoadSection) -> Self {
=        value.0
=    }
=}
=
-impl From<Directions> for usize {
-    fn from(value: Directions) -> Self {
+impl From<RoadSection> for usize {
+    fn from(value: RoadSection) -> Self {
=        value.0 as Self
=    }
=}
=
-impl fmt::Debug for Directions {
+impl fmt::Debug for RoadSection {
=    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "Directions({})", self.to_string())
+        write!(f, "RoadSection({})", self)
=    }
=}
=
-impl Display for Directions {
+impl Display for RoadSection {
=    /// Use Unicode Box Drawing to show where roads are
=    ///
=    /// See https://en.wikipedia.org/wiki/Box_Drawing
@@ -397,163 +326,53 @@ impl Display for Directions {
=}
=
=#[cfg(test)]
-mod roads_tests {
+mod road_sections_tests {
=    use super::*;
=
=    #[test]
=    fn sigils() {
-        assert_eq!(Directions::NONE.to_string(), " ");
-        assert_eq!(Directions::EW.to_string(), "━");
-        assert_eq!(Directions::NS.to_string(), "┃");
-        assert_eq!(Directions::NSW.to_string(), "┫");
+        assert_eq!(RoadSection::default().to_string(), " ");
+        assert_eq!(
+            RoadSection::default()
+                .add_direction(&Direction::East)
+                .add_direction(&Direction::West)
+                .to_string(),
+            "━"
+        );
+        assert_eq!(
+            RoadSection::default()
+                .add_direction(&Direction::North)
+                .add_direction(&Direction::South)
+                .to_string(),
+            "┃"
+        );
+        assert_eq!(
+            RoadSection::default()
+                .add_direction(&Direction::North)
+                .add_direction(&Direction::South)
+                .add_direction(&Direction::West)
+                .to_string(),
+            "┫"
+        );
=    }
=
=    #[test]
=    fn combine_directions() {
-        assert_eq!(Directions::NONE.combine(&Directions::E), Directions::E);
=        assert_eq!(
-            Directions::NONE
-                .combine(&Directions::E)
-                .combine(&Directions::S),
-            Directions::ES
+            RoadSection::default()
+                .combine(&RoadSection::from(Direction::East))
+                .to_owned(),
+            RoadSection::from(Direction::East)
+        );
+        assert_eq!(
+            RoadSection::default()
+                .combine(&RoadSection::from(Direction::East))
+                .combine(&RoadSection::from(Direction::South))
+                .to_owned(),
+            RoadSection::default()
+                .add_direction(&Direction::East)
+                .add_direction(&Direction::South)
+                .to_owned()
=        );
-    }
-}
-
-// SIDES
-
-#[derive(PartialEq, Eq, Clone, Copy, Hash)]
-struct Sides(u8);
-
-impl Sides {
-    fn combine(mut self, other: &Self) -> Self {
-        self.0 |= other.0;
-        self
-    }
-
-    fn overlapping(&self, other: &Self) -> Self {
-        Self(self.0 & other.0)
-    }
-
-    fn overlaps(&self, other: &Self) -> bool {
-        self.overlapping(other).0 != 0b0000
-    }
-
-    const EMPTY: Self = Self(0b0000);
-
-    // One quadrant
-    const NE: Self = Self(0b0001);
-    const ES: Self = Self(0b0010);
-    const SW: Self = Self(0b0100);
-    const WN: Self = Self(0b1000);
-}
-
-impl From<Quadrant> for Sides {
-    fn from(quadrant: Quadrant) -> Self {
-        Self(quadrant as u8)
-    }
-}
-
-impl Default for Sides {
-    fn default() -> Self {
-        Self::EMPTY
-    }
-}
-
-impl From<u8> for Sides {
-    fn from(value: u8) -> Self {
-        Self(value & 0b1111)
-    }
-}
-
-impl From<Sides> for u8 {
-    fn from(value: Sides) -> Self {
-        value.0
-    }
-}
-
-impl fmt::Debug for Sides {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "Sides({})", self.to_string())
-    }
-}
-
-impl Display for Sides {
-    /// Use Unicode Block Elements to show where buildings may go
-    ///
-    /// See https://en.wikipedia.org/wiki/Block_Elements
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let character = match self.0 {
-            // Empty on all sides
-            0b0000 => ' ',
-
-            // One quadrant occupied
-            0b0001 => '▝',
-            0b0010 => '▗',
-            0b0100 => '▖',
-            0b1000 => '▘',
-
-            // One half occupied
-            0b0011 => '▐',
-            0b0101 => '▀',
-            0b1010 => '▄',
-            0b1100 => '▌',
-
-            // Diagonal opposites occupied
-            0b0110 => '▚',
-            0b1001 => '▞',
-
-            // 3 quarters occupied
-            0b0111 => '▟',
-            0b1011 => '▜',
-            0b1101 => '▛',
-            0b1110 => '▙',
-
-            // All sides occupied
-            0b1111 => '█',
-
-            // No other values are possible
-            _ => panic!(
-                "Incorrect bit field passed as a road sides: {value:b}. Only last 4 bits are allowed.",
-                value = self.0
-            ),
-        };
-        write!(f, "{character}")
-    }
-}
-
-#[cfg(test)]
-mod roadsides_tests {
-    use super::*;
-
-    #[test]
-    fn combine() {
-        let mut sides = Sides::EMPTY;
-        assert_eq!(sides.to_string(), " ");
-
-        // Empty and empty gives empty
-        sides = sides.combine(&Sides::EMPTY);
-        assert_eq!(sides.to_string(), " ");
-
-        // Set the north east quadrant as built-up
-        sides = sides.combine(&Sides::NE);
-        assert_eq!(sides.to_string(), "▝");
-
-        // Chaining combinations
-        sides = sides.combine(&Sides::WN).combine(&Sides::ES);
-
-        assert_eq!(sides.to_string(), "▜");
-    }
-
-    #[test]
-    fn overlapping() {
-        let a = Sides::EMPTY;
-        let b = Sides::EMPTY;
-        assert!(a.overlaps(&b).not());
-
-        let a = Sides::SW.combine(&Sides::ES);
-        let b = Sides::ES.combine(&Sides::NE);
-        assert!(a.overlaps(&b));
-        assert_eq!(a.overlapping(&b), Sides::ES);
=    }
=}

Apply all Clippy fixes

On by Tad Lispy

Mostly removing unnecessary cloning.

index 403e493..336504e 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -180,8 +180,8 @@ mod daymonth_tests {
=
=        assert!(DayMonth::new(2000) < DayMonth::new(2001));
=        assert!(DayMonth::new(2001) > DayMonth::new(2000));
-        assert!(DayMonth::new(2000) < DayMonth::new(2000).advance(1.0).to_owned());
-        assert!(DayMonth::new(2001) < DayMonth::new(2000).advance(12.5).to_owned());
-        assert!(DayMonth::new(2000) > DayMonth::new(2000).advance(-0.1).to_owned());
+        assert!(DayMonth::new(2000) < *DayMonth::new(2000).advance(1.0));
+        assert!(DayMonth::new(2001) < *DayMonth::new(2000).advance(12.5));
+        assert!(DayMonth::new(2000) > *DayMonth::new(2000).advance(-0.1));
=    }
=}
index 23fa21d..ebf657f 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -136,7 +136,7 @@ fn spawn_new_districts(
=pub type Snapshot = Vec<District>;
=
=pub fn take_snapshot(districts: Query<&District>) -> Snapshot {
-    districts.iter().map(|district| *district).collect()
+    districts.iter().copied().collect()
=}
=
=fn rollback(
index f8ebcae..6499b6f 100644
--- a/src/explore.rs
+++ b/src/explore.rs
@@ -38,7 +38,7 @@ fn paint_ui(
=                // TODO: Consider using BTreeMap for always sorted snapshots. Or even ditch keys all together and use a vector?
=                .sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
=            {
-                let month = timestamp.month.clone();
+                let month = timestamp.month;
=                let year = timestamp.year;
=                let button = ui.button(format!("{month} {year}"));
=                if button.clicked() {
index a383713..d2acc2e 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -140,7 +140,7 @@ fn take_snapshot(world: &mut World) {
=        history
=            .deref_mut()
=            .snapshots
-            .insert(timestamp.clone(), snapshot);
+            .insert(timestamp, snapshot);
=        let count = history.snapshots.len();
=        info!("Registering snapshot {count} on {timestamp}.")
=    })
@@ -199,10 +199,10 @@ fn replay_historical_events(
=        if entry.date <= date.0 {
=            match entry.event {
=                HistoricalEvent::ConstructionOrder(order) => {
-                    orders.send(order.clone());
+                    orders.send(order);
=                }
=                HistoricalEvent::NewDistrictEstablished(district) => {
-                    districts.send(district.clone());
+                    districts.send(district);
=                }
=            };
=            false

WIP: Parcels and building classes

On by Tad Lispy

It's a mess with a lot of bugs, but some things work. The buildings are placed on parcels on the first spawned district. Unfortunately, the following districts bring parcels that seem to overlap the first few. Looks like the transform of each district is not applied to the transform of parcels.

Also the building class of parcel is ignored and always the same building is spawned. I'll work on this tomorrow.

There are some important changes to the Blender files.

There are only three files that are used:

  1. districts.blend
  2. buildings.blend
  3. roads.blend

With regard to roads, nothing changed. The other two are important.

In buildings.blend there are 3 scenes. Each scene has a collection. Both scene and collection has to have the same name, with the following structure:

building-{class}.{variant}

Class groups building models that can be interchanged, i.e. different models that can be alternatively placed on the same parcel. Variant is a 3 digit number, as provided by blender when making copies. All parts are required!

In the districts.blend, there is the usual ground, but also buildings linked from buildings.blend. Those buildings are reference. At runtime they will be replaced with parcels of the class from reference building. When new buildings are spawned, they will be placed on parcels that match their class. This does not work yet, but we are close.

index 34e3f8d..8283c76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2875,6 +2875,7 @@ dependencies = [
= "itertools",
= "js-sys",
= "rand",
+ "regex",
= "wasm-bindgen",
=]
=
index 5138a14..5989413 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ derive_more = "0.99.17"
=itertools = "0.12.1"
=js-sys = "0.3.68"
=rand = "0.8.5"
+regex = "1.10.3"
=wasm-bindgen = "0.2.91"
=
=[profile.dev.package."*"]
index 2d4e0f3..19d0f64 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differ
index 2067fe1..4607724 100644
Binary files a/art/districts.blend and b/art/districts.blend differ
index 17c6b72..5e8373d 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differ
new file mode 100644
index 0000000..52a9104
Binary files /dev/null and b/assets/districts.glb differ
index 42b67cd..a1796d7 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,27 +1,60 @@
-use crate::date::NewMonth;
-use bevy::prelude::*;
+use crate::{date::NewMonth, districts::Parcel, GameState, PreloadedAssets};
+use bevy::{ecs::query, gltf::Gltf, prelude::*, transform::commands};
=
=pub struct BuildingsPlugin;
=
=impl Plugin for BuildingsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<ConstructionOrder>()
+            .add_systems(Startup, setup_assets)
+            .add_systems(OnEnter(GameState::Simulate), inspect_assets)
=            // Temporarily disabled to make the roads visible
-            // .add_systems(
-            //     Update,
-            //     order_construction.run_if(in_state(GameState::Simulate)),
-            // )
+            .add_systems(
+                Update,
+                order_construction.run_if(in_state(GameState::Simulate)),
+            )
=            .add_systems(Update, spawn_buildings);
=    }
=}
=
+#[derive(Resource)]
+struct BuildingAssets(Handle<Gltf>);
+
+fn setup_assets(
+    assets: Res<AssetServer>,
+    mut commands: Commands,
+    mut preloaded: ResMut<PreloadedAssets>,
+) {
+    const ASSET_PATH: &str = "buildings.glb";
+    info!("Loading {ASSET_PATH} asset");
+    let handle = assets.load(ASSET_PATH);
+    preloaded.0.insert(handle.clone().untyped());
+    commands.insert_resource(BuildingAssets(handle));
+}
+
+fn inspect_assets(
+    building_assets: Res<BuildingAssets>,
+    assets: Res<Assets<Gltf>>,
+    scenes: Res<Assets<Scene>>,
+) {
+    let buildings = assets.get(&building_assets.0).unwrap();
+    for (name, handle) in &buildings.named_scenes {
+        info!("Buildings Scene: {name}");
+    }
+    for (name, handle) in &buildings.named_meshes {
+        info!("Buildings Mesh: {name}");
+    }
+    for (name, handle) in &buildings.named_nodes {
+        info!("Buildings Node: {name}");
+    }
+}
+
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.
=#[derive(Event, Clone, Copy, Debug)]
=pub struct ConstructionOrder {
-    pub x: f32,
-    pub z: f32,
+    pub transform: Transform,
=}
=
=/// A tag for building entities
@@ -35,21 +68,22 @@ fn order_construction(
=    mut new_month_events: EventReader<NewMonth>,
=    mut build_events: EventWriter<ConstructionOrder>,
=    buildings: Query<&Building>,
+    parcels: Query<(Entity, &Parcel, &Transform)>,
+    mut commands: Commands,
=) {
=    if new_month_events.read().count() == 0 {
=        return;
=    }
=
-    let count = buildings.into_iter().count();
-
-    const ROWS: usize = 6;
-    const DISTANCE: usize = 10;
-    let x = (count.rem_euclid(ROWS) * DISTANCE) as f32;
-    let z = (count / ROWS * DISTANCE) as f32;
-
-    info!("Ordering a new construction at ({x:.2}, {z:.2})!");
-
-    build_events.send(ConstructionOrder { x, z });
+    let count = parcels.iter().count();
+    info!("There are {count} parcels now.");
+    for (entity, parcel, transform) in parcels.iter().take(5) {
+        commands.entity(entity).despawn();
+        build_events.send(ConstructionOrder {
+            transform: *transform,
+        });
+        info!("Ordering a new construction at {transform:?}!");
+    }
=}
=
=/// Implement the construction orders
@@ -58,15 +92,17 @@ fn spawn_buildings(
=    assets: ResMut<AssetServer>,
=    mut construction_orders: EventReader<ConstructionOrder>,
=) {
-    for ConstructionOrder { x, z } in construction_orders.read() {
+    for ConstructionOrder { transform } in construction_orders.read() {
+        // TODO: Chose scene by class
=        let model = assets.load(MODEL_PATH);
+        let Vec3 { x, z, .. } = transform.translation;
=
=        info!("Spawning a new building at ({x:.2}, {z:.2})!");
=
=        commands
=            .spawn(SceneBundle {
=                scene: model,
-                transform: Transform::from_xyz(*x, 4., *z),
+                transform: *transform,
=                ..default()
=            })
=            .insert(Building);
index ebf657f..c588d49 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,9 +1,13 @@
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
=use crate::roads::{RoadPlan, RoadsSystems};
-use crate::GameState;
+use crate::{GameState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
+use bevy::ecs::world;
+use bevy::gltf::Gltf;
=use bevy::prelude::*;
+use itertools::Itertools;
+use regex::Regex;
=use std::borrow::Borrow;
=
=pub struct DistrictsPlugin;
@@ -11,7 +15,10 @@ pub struct DistrictsPlugin;
=impl Plugin for DistrictsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<NewDistrictEstablished>()
+            .register_type::<Parcel>()
=            .add_systems(Startup, register_district_systems)
+            .add_systems(Startup, setup_assets)
+            .add_systems(OnEnter(GameState::Simulate), setup_parcels)
=            .add_systems(
=                Update,
=                establish_new_districts.run_if(in_state(GameState::Simulate)),
@@ -20,6 +27,61 @@ impl Plugin for DistrictsPlugin {
=    }
=}
=
+#[derive(Resource)]
+struct DistrictAssets(Handle<Gltf>);
+
+fn setup_assets(
+    assets: Res<AssetServer>,
+    mut commands: Commands,
+    mut preloaded: ResMut<PreloadedAssets>,
+) {
+    const ASSET_PATH: &str = "districts.glb";
+    info!("Loading {ASSET_PATH} asset");
+    let handle = assets.load(ASSET_PATH);
+    preloaded.0.insert(handle.clone().untyped());
+    commands.insert_resource(DistrictAssets(handle));
+}
+
+fn setup_parcels(
+    districts_assets: Res<DistrictAssets>,
+    assets: Res<Assets<Gltf>>,
+    mut scenes: ResMut<Assets<Scene>>,
+) {
+    let building_class_pattern = Regex::new(r"^building-(?<class>.+)\.(?<variant>\d{3})$").unwrap();
+    let districts = assets.get(&districts_assets.0).unwrap();
+
+    // TODO: Do it for every scene
+    let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
+
+    let scene = scenes.get_mut(scene_handle).unwrap();
+
+    let mut query = scene.world.query::<(Entity, &Name, &Transform)>();
+    let parcels: Vec<(Entity, String, Transform)> = query
+        .iter(&scene.world)
+        .filter_map(|(entity, name, transform)| {
+            if let Some(captured) = building_class_pattern.captures(name) {
+                let class = &captured["class"];
+                let variant = &captured["variant"];
+                // FIXME: Looks like parcels from different districts have same transform, i.e. the transform of the district is not applied to the parcels. See errors in the console.
+                Some((entity, class.to_string(), transform.clone()))
+            } else {
+                None
+            }
+        })
+        .collect_vec();
+
+    for (entity, class, transform) in parcels {
+        scene.world.spawn((Parcel { class }, transform));
+        scene.world.despawn(entity);
+    }
+}
+
+#[derive(Component, Reflect, Debug)]
+#[reflect(Component)]
+pub struct Parcel {
+    pub class: String,
+}
+
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.
@@ -175,21 +237,25 @@ fn register_district_systems(world: &mut World) {
=    });
=}
=
-const MODEL_PATH: &str = "district_180x300_a.glb#Scene0";
-
=fn construct_district(
=    In(district): In<District>,
-    assets: ResMut<AssetServer>,
+    districts_assets: Res<DistrictAssets>,
+    assets: Res<Assets<Gltf>>,
=    mut commands: Commands,
=    road_constructors: Res<RoadsSystems>,
=) {
=    info!("Spawning a district: {district:?}");
=
-    let model = assets.load(MODEL_PATH);
+    let districts = assets.get(&districts_assets.0).unwrap();
+
+    // TODO: Do it for every scene
+    let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
+    let transform = Transform::from_translation(district.origin.borrow().into());
+
=    commands
=        .spawn(SceneBundle {
-            scene: model,
-            transform: Transform::from_translation(district.origin.borrow().into()),
+            scene: scene_handle.clone(),
+            transform,
=            ..default()
=        })
=        .insert(district);
index d2acc2e..6330874 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -61,7 +61,8 @@ pub struct EventLogEntry {
=impl Display for HistoricalEvent {
=    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
=        match self {
-            HistoricalEvent::ConstructionOrder(ConstructionOrder { x, z }) => {
+            HistoricalEvent::ConstructionOrder(ConstructionOrder { transform }) => {
+                let Vec3 { x, z, .. } = transform.translation;
=                write!(
=                    f,
=                    "construction of a new building ordered at ({x:.2}, {z:.2})",
@@ -137,10 +138,7 @@ fn take_snapshot(world: &mut World) {
=    };
=
=    world.resource_scope(|_, mut history: Mut<History>| {
-        history
-            .deref_mut()
-            .snapshots
-            .insert(timestamp, snapshot);
+        history.deref_mut().snapshots.insert(timestamp, snapshot);
=        let count = history.snapshots.len();
=        info!("Registering snapshot {count} on {timestamp}.")
=    })
index 61d4417..2e33881 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,5 +1,7 @@
=use std::fmt::Display;
=
+use bevy::math::Vec3;
+
=use crate::buildings::ConstructionOrder;
=use crate::districts::NewDistrictEstablished;
=use crate::history::History;
@@ -32,7 +34,11 @@ impl Display for History {
=            let second = 00;
=
=            match event.event {
-                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder { x, z }) => {
+                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder {
+                    transform,
+                }) => {
+                    let Vec3 { x, z, .. } = transform.translation;
+
=                    writeln!(f, "Insert into buildings (")?;
=                    writeln!(f, "   date,")?;
=                    writeln!(f, "   coordinates")?;

Fix wrong transform of new buildings

On by Tad Lispy

All buildings were placed in the first district. Now they are placed correctly, but there is still a lot of warnings like this:

warning[B0004]: The Base.001 entity with the InheritedVisibility component has a parent without InheritedVisibility.
This will cause inconsistent behaviors! See https://bevyengine.org/learn/errors/#b0004

These warnings were started showing up in previous commit.

index a1796d7..4d931ee 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -68,7 +68,7 @@ fn order_construction(
=    mut new_month_events: EventReader<NewMonth>,
=    mut build_events: EventWriter<ConstructionOrder>,
=    buildings: Query<&Building>,
-    parcels: Query<(Entity, &Parcel, &Transform)>,
+    parcels: Query<(Entity, &Parcel, &GlobalTransform)>,
=    mut commands: Commands,
=) {
=    if new_month_events.read().count() == 0 {
@@ -80,7 +80,7 @@ fn order_construction(
=    for (entity, parcel, transform) in parcels.iter().take(5) {
=        commands.entity(entity).despawn();
=        build_events.send(ConstructionOrder {
-            transform: *transform,
+            transform: transform.to_owned().into(),
=        });
=        info!("Ordering a new construction at {transform:?}!");
=    }
index c588d49..df34180 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -62,7 +62,6 @@ fn setup_parcels(
=            if let Some(captured) = building_class_pattern.captures(name) {
=                let class = &captured["class"];
=                let variant = &captured["variant"];
-                // FIXME: Looks like parcels from different districts have same transform, i.e. the transform of the district is not applied to the parcels. See errors in the console.
=                Some((entity, class.to_string(), transform.clone()))
=            } else {
=                None
@@ -71,7 +70,14 @@ fn setup_parcels(
=        .collect_vec();
=
=    for (entity, class, transform) in parcels {
-        scene.world.spawn((Parcel { class }, transform));
+        scene
+            .world
+            .spawn(SpatialBundle {
+                transform,
+                visibility: Visibility::Visible,
+                ..default()
+            })
+            .insert(Parcel { class });
=        scene.world.despawn(entity);
=    }
=}
index 2e33881..3a34b07 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,10 +1,8 @@
-use std::fmt::Display;
-
-use bevy::math::Vec3;
-
=use crate::buildings::ConstructionOrder;
=use crate::districts::NewDistrictEstablished;
=use crate::history::History;
+use bevy::math::Vec3;
+use std::fmt::Display;
=
=pub fn export(history: &History) -> String {
=    history.to_string()

Enable World Inspector plugin to help in debugging

On by Tad Lispy

Turns out we can use booth bevy_inspector_egui and bevy_egui. The trick is not to add EguiPlugin manually and let WorldInspectorPlugin add it automatically. Otherwise there is a runtime error saying that EguiPlugin was already initialized or something like that.

index 8283c76..91487cb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
=
=[[package]]
=name = "accesskit"
-version = "0.12.2"
+version = "0.12.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cb10ed32c63247e4e39a8f42e8e30fb9442fbf7878c8e4a9849e7e381619bea"
+checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b"
=
=[[package]]
=name = "accesskit_consumer"
@@ -108,14 +108,13 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
=
=[[package]]
=name = "alsa"
-version = "0.7.1"
+version = "0.9.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47"
+checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce"
=dependencies = [
= "alsa-sys",
- "bitflags 1.3.2",
+ "bitflags 2.4.2",
= "libc",
- "nix 0.24.3",
=]
=
=[[package]]
@@ -138,14 +137,14 @@ dependencies = [
= "bitflags 2.4.2",
= "cc",
= "cesu8",
- "jni 0.21.1",
+ "jni",
= "jni-sys",
= "libc",
= "log",
- "ndk 0.8.0",
+ "ndk",
= "ndk-context",
- "ndk-sys 0.5.0+25.2.9519653",
- "num_enum 0.7.2",
+ "ndk-sys",
+ "num_enum",
= "thiserror",
=]
=
@@ -317,6 +316,44 @@ dependencies = [
= "bevy_internal",
=]
=
+[[package]]
+name = "bevy-inspector-egui"
+version = "0.23.4"
+source = "git+https://github.com/jakobhellermann/bevy-inspector-egui?rev=1563fe9#1563fe945c81005db785c0109cf8ea6a3ef86f75"
+dependencies = [
+ "bevy-inspector-egui-derive",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_core_pipeline",
+ "bevy_ecs",
+ "bevy_egui",
+ "bevy_hierarchy",
+ "bevy_log",
+ "bevy_math",
+ "bevy_pbr",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_time",
+ "bevy_utils",
+ "bevy_window",
+ "egui",
+ "image",
+ "once_cell",
+ "pretty-type-name",
+ "smallvec",
+]
+
+[[package]]
+name = "bevy-inspector-egui-derive"
+version = "0.23.0"
+source = "git+https://github.com/jakobhellermann/bevy-inspector-egui?rev=1563fe9#1563fe945c81005db785c0109cf8ea6a3ef86f75"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
=[[package]]
=name = "bevy_a11y"
=version = "0.13.0"
@@ -422,7 +459,7 @@ dependencies = [
= "bevy_reflect",
= "bevy_transform",
= "bevy_utils",
- "oboe",
+ "oboe 0.5.0",
= "rodio",
=]
=
@@ -720,7 +757,7 @@ dependencies = [
= "quote",
= "rustc-hash",
= "syn 2.0.52",
- "toml_edit 0.21.1",
+ "toml_edit",
=]
=
=[[package]]
@@ -744,9 +781,9 @@ dependencies = [
=
=[[package]]
=name = "bevy_panorbit_camera"
-version = "0.16.0"
+version = "0.16.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28244cb96f651b603cb98b4b58145939ebdb362331347a827849750d3b5bc99f"
+checksum = "c2ae36af26f6c75067cba3fac09fbd4cbe7fc565492cc6064338a9e0063363db"
=dependencies = [
= "bevy",
=]
@@ -1132,9 +1169,9 @@ dependencies = [
=
=[[package]]
=name = "blake3"
-version = "1.5.0"
+version = "1.5.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87"
+checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52"
=dependencies = [
= "arrayref",
= "arrayvec",
@@ -1205,24 +1242,24 @@ dependencies = [
=
=[[package]]
=name = "bumpalo"
-version = "3.15.3"
+version = "3.15.4"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
+checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
=
=[[package]]
=name = "bytemuck"
-version = "1.14.3"
+version = "1.15.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
+checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
=dependencies = [
= "bytemuck_derive",
=]
=
=[[package]]
=name = "bytemuck_derive"
-version = "1.5.0"
+version = "1.6.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1"
+checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60"
=dependencies = [
= "proc-macro2",
= "quote",
@@ -1269,9 +1306,9 @@ dependencies = [
=
=[[package]]
=name = "cc"
-version = "1.0.89"
+version = "1.0.90"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
=dependencies = [
= "jobserver",
= "libc",
@@ -1312,7 +1349,7 @@ checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
=dependencies = [
= "glob",
= "libc",
- "libloading 0.8.2",
+ "libloading 0.8.3",
=]
=
=[[package]]
@@ -1501,27 +1538,25 @@ dependencies = [
=
=[[package]]
=name = "cpal"
-version = "0.15.2"
+version = "0.15.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
+checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
=dependencies = [
= "alsa",
= "core-foundation-sys",
= "coreaudio-rs",
= "dasp_sample",
- "jni 0.19.0",
+ "jni",
= "js-sys",
= "libc",
= "mach2",
- "ndk 0.7.0",
+ "ndk",
= "ndk-context",
- "oboe",
- "once_cell",
- "parking_lot",
+ "oboe 0.6.1",
= "wasm-bindgen",
= "wasm-bindgen-futures",
= "web-sys",
- "windows 0.46.0",
+ "windows 0.54.0",
=]
=
=[[package]]
@@ -1561,7 +1596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
=dependencies = [
= "bitflags 2.4.2",
- "libloading 0.8.2",
+ "libloading 0.8.3",
= "winapi",
=]
=
@@ -1602,7 +1637,7 @@ version = "0.5.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
=dependencies = [
- "libloading 0.8.2",
+ "libloading 0.8.3",
=]
=
=[[package]]
@@ -1701,9 +1736,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
=
=[[package]]
=name = "erased-serde"
-version = "0.4.3"
+version = "0.4.4"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "388979d208a049ffdfb22fa33b9c81942215b940910bccfe258caeb25d125cb3"
+checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3"
=dependencies = [
= "serde",
=]
@@ -1904,9 +1939,9 @@ dependencies = [
=
=[[package]]
=name = "gilrs"
-version = "0.10.4"
+version = "0.10.5"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b2e57a9cb946b5d04ae8638c5f554abb5a9f82c4c950fd5b1fee6d119592fb"
+checksum = "0510502768c64b944bf4d1518ba36e2431c638ac996ebacb543a297b145f8300"
=dependencies = [
= "fnv",
= "gilrs-core",
@@ -1917,9 +1952,9 @@ dependencies = [
=
=[[package]]
=name = "gilrs-core"
-version = "0.5.10"
+version = "0.5.11"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0af1827b7dd2f36d740ae804c1b3ea0d64c12533fb61ff91883005143a0e8c5a"
+checksum = "85c132270a155f2548e67d66e731075c336c39098afc555752f3df8f882c720e"
=dependencies = [
= "core-foundation",
= "inotify",
@@ -1928,12 +1963,12 @@ dependencies = [
= "libc",
= "libudev-sys",
= "log",
- "nix 0.27.1",
+ "nix",
= "uuid",
= "vec_map",
= "wasm-bindgen",
= "web-sys",
- "windows 0.52.0",
+ "windows 0.54.0",
=]
=
=[[package]]
@@ -2119,7 +2154,7 @@ dependencies = [
= "bitflags 2.4.2",
= "com",
= "libc",
- "libloading 0.8.2",
+ "libloading 0.8.3",
= "thiserror",
= "widestring",
= "winapi",
@@ -2246,34 +2281,6 @@ version = "1.0.10"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
=
-[[package]]
-name = "jni"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
-dependencies = [
- "cesu8",
- "combine",
- "jni-sys",
- "log",
- "thiserror",
- "walkdir",
-]
-
-[[package]]
-name = "jni"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
-dependencies = [
- "cesu8",
- "combine",
- "jni-sys",
- "log",
- "thiserror",
- "walkdir",
-]
-
=[[package]]
=name = "jni"
=version = "0.21.1"
@@ -2327,7 +2334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
=dependencies = [
= "libc",
- "libloading 0.8.2",
+ "libloading 0.8.3",
= "pkg-config",
=]
=
@@ -2387,9 +2394,9 @@ dependencies = [
=
=[[package]]
=name = "libloading"
-version = "0.8.2"
+version = "0.8.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
+checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
=dependencies = [
= "cfg-if",
= "windows-targets 0.52.4",
@@ -2552,20 +2559,6 @@ dependencies = [
= "unicode-ident",
=]
=
-[[package]]
-name = "ndk"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
-dependencies = [
- "bitflags 1.3.2",
- "jni-sys",
- "ndk-sys 0.4.1+23.1.7779620",
- "num_enum 0.5.11",
- "raw-window-handle 0.5.2",
- "thiserror",
-]
-
=[[package]]
=name = "ndk"
=version = "0.8.0"
@@ -2575,8 +2568,8 @@ dependencies = [
= "bitflags 2.4.2",
= "jni-sys",
= "log",
- "ndk-sys 0.5.0+25.2.9519653",
- "num_enum 0.7.2",
+ "ndk-sys",
+ "num_enum",
= "raw-window-handle 0.6.0",
= "thiserror",
=]
@@ -2587,15 +2580,6 @@ version = "0.1.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
=
-[[package]]
-name = "ndk-sys"
-version = "0.4.1+23.1.7779620"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
-dependencies = [
- "jni-sys",
-]
-
=[[package]]
=name = "ndk-sys"
=version = "0.5.0+25.2.9519653"
@@ -2607,23 +2591,13 @@ dependencies = [
=
=[[package]]
=name = "nix"
-version = "0.24.3"
+version = "0.28.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
-dependencies = [
- "bitflags 1.3.2",
- "cfg-if",
- "libc",
-]
-
-[[package]]
-name = "nix"
-version = "0.27.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
=dependencies = [
= "bitflags 2.4.2",
= "cfg-if",
+ "cfg_aliases",
= "libc",
=]
=
@@ -2680,21 +2654,23 @@ dependencies = [
=]
=
=[[package]]
-name = "num-traits"
-version = "0.2.18"
+name = "num-derive"
+version = "0.4.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
=dependencies = [
- "autocfg",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
=]
=
=[[package]]
-name = "num_enum"
-version = "0.5.11"
+name = "num-traits"
+version = "0.2.18"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
=dependencies = [
- "num_enum_derive 0.5.11",
+ "autocfg",
=]
=
=[[package]]
@@ -2703,19 +2679,7 @@ version = "0.7.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
=dependencies = [
- "num_enum_derive 0.7.2",
-]
-
-[[package]]
-name = "num_enum_derive"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
-dependencies = [
- "proc-macro-crate 1.3.1",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
+ "num_enum_derive",
=]
=
=[[package]]
@@ -2724,7 +2688,7 @@ version = "0.7.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
=dependencies = [
- "proc-macro-crate 3.1.0",
+ "proc-macro-crate",
= "proc-macro2",
= "quote",
= "syn 2.0.52",
@@ -2823,12 +2787,23 @@ version = "0.5.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0"
=dependencies = [
- "jni 0.20.0",
- "ndk 0.7.0",
+ "num-derive 0.3.3",
+ "num-traits",
+ "oboe-sys 0.5.0",
+]
+
+[[package]]
+name = "oboe"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
+dependencies = [
+ "jni",
+ "ndk",
= "ndk-context",
- "num-derive",
+ "num-derive 0.4.2",
= "num-traits",
- "oboe-sys",
+ "oboe-sys 0.6.1",
=]
=
=[[package]]
@@ -2840,6 +2815,15 @@ dependencies = [
= "cc",
=]
=
+[[package]]
+name = "oboe-sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
+dependencies = [
+ "cc",
+]
+
=[[package]]
=name = "ogg"
=version = "0.8.0"
@@ -2869,6 +2853,7 @@ name = "otterhide"
=version = "0.1.0"
=dependencies = [
= "bevy",
+ "bevy-inspector-egui",
= "bevy_egui",
= "bevy_panorbit_camera",
= "derive_more",
@@ -3017,14 +3002,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
=
=[[package]]
-name = "proc-macro-crate"
-version = "1.3.1"
+name = "pretty-type-name"
+version = "1.0.1"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
-dependencies = [
- "once_cell",
- "toml_edit 0.19.15",
-]
+checksum = "f0f73cdaf19b52e6143685c3606206e114a4dfa969d6b14ec3894c88eb38bd4b"
=
=[[package]]
=name = "proc-macro-crate"
@@ -3032,14 +3013,14 @@ version = "3.1.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
=dependencies = [
- "toml_edit 0.21.1",
+ "toml_edit",
=]
=
=[[package]]
=name = "proc-macro2"
-version = "1.0.78"
+version = "1.0.79"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
=dependencies = [
= "unicode-ident",
=]
@@ -3439,9 +3420,9 @@ checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
=
=[[package]]
=name = "svg_fmt"
-version = "0.4.1"
+version = "0.4.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+checksum = "f83ba502a3265efb76efb89b0a2f7782ad6f2675015d4ce37e4b547dda42b499"
=
=[[package]]
=name = "syn"
@@ -3467,9 +3448,9 @@ dependencies = [
=
=[[package]]
=name = "sysinfo"
-version = "0.30.6"
+version = "0.30.7"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6746919caf9f2a85bff759535664c060109f21975c5ac2e8652e60102bd4d196"
+checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18"
=dependencies = [
= "cfg-if",
= "core-foundation-sys",
@@ -3502,18 +3483,18 @@ dependencies = [
=
=[[package]]
=name = "thiserror"
-version = "1.0.57"
+version = "1.0.58"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
=dependencies = [
= "thiserror-impl",
=]
=
=[[package]]
=name = "thiserror-impl"
-version = "1.0.57"
+version = "1.0.58"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
=dependencies = [
= "proc-macro2",
= "quote",
@@ -3587,17 +3568,6 @@ version = "0.6.5"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
=
-[[package]]
-name = "toml_edit"
-version = "0.19.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
-dependencies = [
- "indexmap",
- "toml_datetime",
- "winnow",
-]
-
=[[package]]
=name = "toml_edit"
=version = "0.21.1"
@@ -3998,13 +3968,13 @@ dependencies = [
=
=[[package]]
=name = "webbrowser"
-version = "0.8.12"
+version = "0.8.13"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71"
+checksum = "d1b04c569c83a9bb971dd47ec6fd48753315f4bf989b9b04a2e7ca4d7f0dc950"
=dependencies = [
= "core-foundation",
= "home",
- "jni 0.21.1",
+ "jni",
= "log",
= "ndk-context",
= "objc",
@@ -4094,11 +4064,11 @@ dependencies = [
= "js-sys",
= "khronos-egl",
= "libc",
- "libloading 0.8.2",
+ "libloading 0.8.3",
= "log",
= "metal",
= "naga",
- "ndk-sys 0.5.0+25.2.9519653",
+ "ndk-sys",
= "objc",
= "once_cell",
= "parking_lot",
@@ -4163,15 +4133,6 @@ version = "0.4.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
=
-[[package]]
-name = "windows"
-version = "0.46.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25"
-dependencies = [
- "windows-targets 0.42.2",
-]
-
=[[package]]
=name = "windows"
=version = "0.48.0"
@@ -4189,7 +4150,17 @@ version = "0.52.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
=dependencies = [
- "windows-core",
+ "windows-core 0.52.0",
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows"
+version = "0.54.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
+dependencies = [
+ "windows-core 0.54.0",
= "windows-targets 0.52.4",
=]
=
@@ -4202,6 +4173,16 @@ dependencies = [
= "windows-targets 0.52.4",
=]
=
+[[package]]
+name = "windows-core"
+version = "0.54.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
+dependencies = [
+ "windows-result",
+ "windows-targets 0.52.4",
+]
+
=[[package]]
=name = "windows-implement"
=version = "0.48.0"
@@ -4224,6 +4205,15 @@ dependencies = [
= "syn 1.0.109",
=]
=
+[[package]]
+name = "windows-result"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
=[[package]]
=name = "windows-sys"
=version = "0.45.0"
@@ -4424,9 +4414,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
=
=[[package]]
=name = "winit"
-version = "0.29.13"
+version = "0.29.14"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b9d7047a2a569d5a81e3be098dcd8153759909b127477f4397e03cf1006d90a"
+checksum = "a7a3db69ffbe53a9babec7804da7a90f21020fcce1f2f5e5291e2311245b993d"
=dependencies = [
= "ahash",
= "android-activity",
@@ -4443,8 +4433,8 @@ dependencies = [
= "libc",
= "log",
= "memmap2",
- "ndk 0.8.0",
- "ndk-sys 0.5.0+25.2.9519653",
+ "ndk",
+ "ndk-sys",
= "objc2 0.4.1",
= "once_cell",
= "orbclient",
@@ -4499,7 +4489,7 @@ dependencies = [
= "as-raw-xcb-connection",
= "gethostname",
= "libc",
- "libloading 0.8.2",
+ "libloading 0.8.3",
= "once_cell",
= "rustix",
= "x11rb-protocol",
index 5989413..698fa47 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2021"
=
=[dependencies]
=bevy = { version = "0.13", features = ["wayland"] }
+bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector-egui", rev = "1563fe9", version = "0.23.4" }
=bevy_egui = "0.25.0"
=bevy_panorbit_camera = "0.16.0"
=derive_more = "0.99.17"
index 668e211..e9f4e77 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,6 +14,7 @@ mod sun;
=use bevy::prelude::*;
=use bevy::utils::HashSet;
=use bevy_egui::EguiPlugin;
+use bevy_inspector_egui::quick::WorldInspectorPlugin;
=use buildings::BuildingsPlugin;
=use camera::CameraPlugin;
=use date::Date;
@@ -55,8 +56,8 @@ fn main() {
=        .add_plugins(DatePlugin)
=        .add_plugins(SunPlugin)
=        .add_plugins(HistoryPlugin)
+        .add_plugins(WorldInspectorPlugin::new())
=        .add_plugins(ExplorePlugin)
-        .add_plugins(EguiPlugin)
=        .add_systems(Startup, greet)
=        .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
=        .add_systems(Update, switch_states)

Fix the parent without GlobalTransform warnings

On by Tad Lispy

It's because reference houses were despawned, but all their children were left orphaned.

index df34180..1026c9c 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -78,7 +78,7 @@ fn setup_parcels(
=                ..default()
=            })
=            .insert(Parcel { class });
-        scene.world.despawn(entity);
+        scene.world.entity_mut(entity).despawn_recursive();
=    }
=}
=

Make some systems oblivious to new month

On by Tad Lispy

The new districts and new buildings systems are run once a month (for now), but internally they don't need to know that. With this change, it's the plugin constructor that controls how often the systems run. Systems just do their job. One practical benefit, is that we can run those systems on different schedules, for example on startup and also once a month, without having to duplicate them.

index 4d931ee..742ea57 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -8,10 +8,10 @@ impl Plugin for BuildingsPlugin {
=        app.add_event::<ConstructionOrder>()
=            .add_systems(Startup, setup_assets)
=            .add_systems(OnEnter(GameState::Simulate), inspect_assets)
-            // Temporarily disabled to make the roads visible
=            .add_systems(
=                Update,
-                order_construction.run_if(in_state(GameState::Simulate)),
+                order_construction
+                    .run_if(on_event::<NewMonth>().and_then(in_state(GameState::Simulate))),
=            )
=            .add_systems(Update, spawn_buildings);
=    }
@@ -65,16 +65,11 @@ const MODEL_PATH: &str = "large_buildingB.glb#Scene0";
=
=/// Issue a construction order, for the record and effect
=fn order_construction(
-    mut new_month_events: EventReader<NewMonth>,
=    mut build_events: EventWriter<ConstructionOrder>,
=    buildings: Query<&Building>,
=    parcels: Query<(Entity, &Parcel, &GlobalTransform)>,
=    mut commands: Commands,
=) {
-    if new_month_events.read().count() == 0 {
-        return;
-    }
-
=    let count = parcels.iter().count();
=    info!("There are {count} parcels now.");
=    for (entity, parcel, transform) in parcels.iter().take(5) {
index 1026c9c..07cacbd 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -21,7 +21,8 @@ impl Plugin for DistrictsPlugin {
=            .add_systems(OnEnter(GameState::Simulate), setup_parcels)
=            .add_systems(
=                Update,
-                establish_new_districts.run_if(in_state(GameState::Simulate)),
+                establish_new_districts
+                    .run_if(on_event::<NewMonth>().and_then(in_state(GameState::Simulate))),
=            )
=            .add_systems(Update, spawn_new_districts);
=    }
@@ -161,14 +162,9 @@ impl District {
=}
=
=fn establish_new_districts(
-    mut new_month_events: EventReader<NewMonth>,
=    mut new_districts: EventWriter<NewDistrictEstablished>,
=    buildings: Query<&District>,
=) {
-    if new_month_events.read().count() == 0 {
-        return;
-    }
-
=    let count = buildings.into_iter().count();
=
=    const ROWS: usize = 5;

Remove some unused imports and parameters

On by Tad Lispy

index 742ea57..76aea7c 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,5 +1,8 @@
-use crate::{date::NewMonth, districts::Parcel, GameState, PreloadedAssets};
-use bevy::{ecs::query, gltf::Gltf, prelude::*, transform::commands};
+use crate::date::NewMonth;
+use crate::districts::Parcel;
+use crate::{GameState, PreloadedAssets};
+use bevy::gltf::Gltf;
+use bevy::prelude::*;
=
=pub struct BuildingsPlugin;
=
@@ -32,11 +35,7 @@ fn setup_assets(
=    commands.insert_resource(BuildingAssets(handle));
=}
=
-fn inspect_assets(
-    building_assets: Res<BuildingAssets>,
-    assets: Res<Assets<Gltf>>,
-    scenes: Res<Assets<Scene>>,
-) {
+fn inspect_assets(building_assets: Res<BuildingAssets>, assets: Res<Assets<Gltf>>) {
=    let buildings = assets.get(&building_assets.0).unwrap();
=    for (name, handle) in &buildings.named_scenes {
=        info!("Buildings Scene: {name}");

Update the buildings.glb export

On by Tad Lispy

The export from .blend to .glb should really be automated.

index 5e8373d..724844e 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differ

Give names to district and road entities

On by Tad Lispy

The names are used in the world inspector UI, and it really makes debugging easier.

index 07cacbd..3cd091e 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -260,7 +260,8 @@ fn construct_district(
=            transform,
=            ..default()
=        })
-        .insert(district);
+        .insert(district)
+        .insert(Name::new("District"));
=
=    commands.run_system_with_input(
=        road_constructors.implement_road_plan,
index f8b5e88..64921ba 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -103,6 +103,7 @@ fn construct_road_section(
=                        ..default()
=                    })
=                    .insert(new_section)
+                    .insert(Name::new("Road"))
=                    .id();
=                debug!("Laying a new road section {new_section:?} at {coordinates:?} ({entity:?})");
=
@@ -124,6 +125,7 @@ fn construct_road_section(
=                        ..default()
=                    })
=                    .insert(section)
+                    .insert(Name::new("Road"))
=                    .id();
=                debug!("Combined road section {section:?} at {coordinates:?} ({entity:?})");
=

Use different building models from buildings.blend

On by Tad Lispy

Now every month one building is ordered from every class. We have 2 classes so far, so every month 2 buildings are ordered and spawned. The small building has 2 varieties, and every time a random one is chosen.

index 76aea7c..4516ab8 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -3,14 +3,20 @@ use crate::districts::Parcel;
=use crate::{GameState, PreloadedAssets};
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
+use bevy::utils::HashMap;
+use itertools::Itertools;
+use rand::seq::IteratorRandom;
+use regex::Regex;
=
=pub struct BuildingsPlugin;
=
=impl Plugin for BuildingsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<ConstructionOrder>()
+            .init_resource::<BuildingModels>()
+            .register_type::<BuildingModels>()
=            .add_systems(Startup, setup_assets)
-            .add_systems(OnEnter(GameState::Simulate), inspect_assets)
+            .add_systems(OnEnter(GameState::Simulate), setup_building_models)
=            .add_systems(
=                Update,
=                order_construction
@@ -35,25 +41,73 @@ fn setup_assets(
=    commands.insert_resource(BuildingAssets(handle));
=}
=
-fn inspect_assets(building_assets: Res<BuildingAssets>, assets: Res<Assets<Gltf>>) {
-    let buildings = assets.get(&building_assets.0).unwrap();
-    for (name, handle) in &buildings.named_scenes {
-        info!("Buildings Scene: {name}");
+#[derive(Resource, Default, Debug, Reflect)]
+#[reflect(Resource)]
+pub struct BuildingModels(pub HashMap<String, BuildingModel>);
+
+#[derive(Debug, Reflect)]
+pub struct BuildingModel {
+    pub class: String,
+    pub scene: Handle<Scene>,
+}
+
+impl BuildingModels {
+    pub fn classes(&self) -> Vec<String> {
+        self.0
+            .values()
+            .map(|BuildingModel { class, .. }| class)
+            .unique()
+            .cloned()
+            .collect()
=    }
-    for (name, handle) in &buildings.named_meshes {
-        info!("Buildings Mesh: {name}");
+
+    pub fn get_class(&self, class: &str) -> HashMap<String, Handle<Scene>> {
+        let mut models = HashMap::default();
+        for (name, model) in self.0.iter() {
+            if model.class == class {
+                models.insert(name.to_string(), model.scene.clone());
+            }
+        }
+        models
=    }
-    for (name, handle) in &buildings.named_nodes {
-        info!("Buildings Node: {name}");
+}
+
+pub fn building_class(name: &str) -> Option<String> {
+    let building_class_pattern = Regex::new(r"^building-(?<class>.+)\.(?<variant>\d{3})$").unwrap();
+    building_class_pattern
+        .captures(name)
+        .map(|captured| captured["class"].to_string())
+}
+
+fn setup_building_models(
+    building_assets: Res<BuildingAssets>,
+    assets: Res<Assets<Gltf>>,
+    mut models: ResMut<BuildingModels>,
+) {
+    let buildings = assets.get(&building_assets.0).unwrap();
+    for (name, scene) in &buildings.named_scenes {
+        info!("Looking for buildings in scene {name}");
+        if let Some(class) = building_class(name) {
+            info!("Registering {name} in class {class}");
+
+            models.0.insert(
+                name.to_string(),
+                BuildingModel {
+                    class,
+                    scene: scene.clone(),
+                },
+            );
+        }
=    }
=}
=
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.
-#[derive(Event, Clone, Copy, Debug)]
+#[derive(Event, Clone, Debug)]
=pub struct ConstructionOrder {
=    pub transform: Transform,
+    pub model: String,
=}
=
=/// A tag for building entities
@@ -65,41 +119,58 @@ const MODEL_PATH: &str = "large_buildingB.glb#Scene0";
=/// Issue a construction order, for the record and effect
=fn order_construction(
=    mut build_events: EventWriter<ConstructionOrder>,
-    buildings: Query<&Building>,
=    parcels: Query<(Entity, &Parcel, &GlobalTransform)>,
=    mut commands: Commands,
+    models: Res<BuildingModels>,
=) {
=    let count = parcels.iter().count();
=    info!("There are {count} parcels now.");
-    for (entity, parcel, transform) in parcels.iter().take(5) {
+
+    for class in models.classes() {
+        info!("Let's build a {class}");
+        // Find parcel that matches this class
+        let Some((entity, _, transform)) = parcels
+            .iter()
+            .filter(|(_, parcel, _)| parcel.class == class)
+            .next()
+        else {
+            warn!("Can't build {class}. No suitable parcels.");
+            continue;
+        };
+        let models = &models.get_class(&class);
+        let model = models.keys().choose(&mut rand::thread_rng()).unwrap();
+
+        // Remove parcel
=        commands.entity(entity).despawn();
+
+        // Order the construction
=        build_events.send(ConstructionOrder {
=            transform: transform.to_owned().into(),
+            model: model.to_string(),
=        });
-        info!("Ordering a new construction at {transform:?}!");
=    }
=}
=
=/// Implement the construction orders
=fn spawn_buildings(
=    mut commands: Commands,
-    assets: ResMut<AssetServer>,
=    mut construction_orders: EventReader<ConstructionOrder>,
+    models: Res<BuildingModels>,
=) {
-    for ConstructionOrder { transform } in construction_orders.read() {
-        // TODO: Chose scene by class
-        let model = assets.load(MODEL_PATH);
-        let Vec3 { x, z, .. } = transform.translation;
+    for order in construction_orders.read() {
+        let Vec3 { x, z, .. } = order.transform.translation;
+        let model = models.0.get(&order.model).unwrap();
=
=        info!("Spawning a new building at ({x:.2}, {z:.2})!");
=
=        commands
=            .spawn(SceneBundle {
-                scene: model,
-                transform: *transform,
+                scene: model.scene.clone(),
+                transform: order.transform,
=                ..default()
=            })
-            .insert(Building);
+            .insert(Building)
+            .insert(Name::new(order.model.clone()));
=    }
=}
=
index 3cd091e..9709fc1 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,13 +1,12 @@
+use crate::buildings::building_class;
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
=use crate::roads::{RoadPlan, RoadsSystems};
=use crate::{GameState, PreloadedAssets};
=use bevy::ecs::system::SystemId;
-use bevy::ecs::world;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
=use itertools::Itertools;
-use regex::Regex;
=use std::borrow::Borrow;
=
=pub struct DistrictsPlugin;
@@ -48,22 +47,18 @@ fn setup_parcels(
=    assets: Res<Assets<Gltf>>,
=    mut scenes: ResMut<Assets<Scene>>,
=) {
-    let building_class_pattern = Regex::new(r"^building-(?<class>.+)\.(?<variant>\d{3})$").unwrap();
=    let districts = assets.get(&districts_assets.0).unwrap();
=
=    // TODO: Do it for every scene
=    let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
=
=    let scene = scenes.get_mut(scene_handle).unwrap();
-
=    let mut query = scene.world.query::<(Entity, &Name, &Transform)>();
=    let parcels: Vec<(Entity, String, Transform)> = query
=        .iter(&scene.world)
=        .filter_map(|(entity, name, transform)| {
-            if let Some(captured) = building_class_pattern.captures(name) {
-                let class = &captured["class"];
-                let variant = &captured["variant"];
-                Some((entity, class.to_string(), transform.clone()))
+            if let Some(class) = building_class(name) {
+                Some((entity, class, transform.clone()))
=            } else {
=                None
=            }
index 6330874..9e220bc 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -45,14 +45,14 @@ pub struct Future {
=}
=
=/// A wrapper for any kind of event that should be replayed in explore mode
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Debug)]
=pub enum HistoricalEvent {
=    ConstructionOrder(buildings::ConstructionOrder),
=    NewDistrictEstablished(districts::NewDistrictEstablished),
=}
=
=/// A historical event together with it's date
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Debug)]
=pub struct EventLogEntry {
=    pub date: DayMonth,
=    pub event: HistoricalEvent,
@@ -61,11 +61,11 @@ pub struct EventLogEntry {
=impl Display for HistoricalEvent {
=    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
=        match self {
-            HistoricalEvent::ConstructionOrder(ConstructionOrder { transform }) => {
+            HistoricalEvent::ConstructionOrder(ConstructionOrder { transform, model }) => {
=                let Vec3 { x, z, .. } = transform.translation;
=                write!(
=                    f,
-                    "construction of a new building ordered at ({x:.2}, {z:.2})",
+                    "construction of a new building ({model}) ordered at ({x:.2}, {z:.2})",
=                )
=            }
=            HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
@@ -195,7 +195,7 @@ fn replay_historical_events(
=) {
=    future.events.retain(|entry| {
=        if entry.date <= date.0 {
-            match entry.event {
+            match entry.event.clone() {
=                HistoricalEvent::ConstructionOrder(order) => {
=                    orders.send(order);
=                }
index e9f4e77..d051364 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,7 +13,6 @@ mod sun;
=
=use bevy::prelude::*;
=use bevy::utils::HashSet;
-use bevy_egui::EguiPlugin;
=use bevy_inspector_egui::quick::WorldInspectorPlugin;
=use buildings::BuildingsPlugin;
=use camera::CameraPlugin;
index 3a34b07..119e9aa 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -31,20 +31,23 @@ impl Display for History {
=            let minute = date.minute() as i32;
=            let second = 00;
=
-            match event.event {
+            match event.event.clone() {
=                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder {
=                    transform,
+                    model,
=                }) => {
=                    let Vec3 { x, z, .. } = transform.translation;
=
=                    writeln!(f, "Insert into buildings (")?;
=                    writeln!(f, "   date,")?;
+                    writeln!(f, "   model,")?;
=                    writeln!(f, "   coordinates")?;
=                    writeln!(f, ") values (")?;
=                    writeln!(
=                        f,
=                        "   {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
=                    )?;
+                    writeln!(f, "   {model}")?;
=                    writeln!(f, "   ({x:.3}, {z:.3})")?;
=                    writeln!(f, ");")?;
=                }

Upgrade Nix dependencies

On by Tad Lispy

index 4c2cd61..1e07b65 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
=        "systems": "systems"
=      },
=      "locked": {
-        "lastModified": 1709126324,
-        "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
=        "owner": "numtide",
=        "repo": "flake-utils",
-        "rev": "d465f4819400de7c8d874d50b982301f28a84605",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
=        "type": "github"
=      },
=      "original": {
@@ -38,11 +38,11 @@
=    },
=    "nixpkgs": {
=      "locked": {
-        "lastModified": 1709237383,
-        "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
+        "lastModified": 1710272261,
+        "narHash": "sha256-g0bDwXFmTE7uGDOs9HcJsfLFhH7fOsASbAuOzDC+fhQ=",
=        "owner": "NixOS",
=        "repo": "nixpkgs",
-        "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
+        "rev": "0ad13a6833440b8e238947e47bea7f11071dc2b2",
=        "type": "github"
=      },
=      "original": {
@@ -81,11 +81,11 @@
=        "nixpkgs": "nixpkgs_2"
=      },
=      "locked": {
-        "lastModified": 1709519692,
-        "narHash": "sha256-+ICGcASuUGpx82io6FVkMW7Pv4dvEl1v9A0ZtBKT41A=",
+        "lastModified": 1710382258,
+        "narHash": "sha256-2FW1q+o34VBweYQiEkRaSEkNMq3ecrn83VzETeGiVbY=",
=        "owner": "oxalica",
=        "repo": "rust-overlay",
-        "rev": "30c3af18405567115958c577c62548bdc5a251e7",
+        "rev": "8ce81e71ab04a7e906fae62da086d6ee5d6cfc21",
=        "type": "github"
=      },
=      "original": {

Drop unused constant

On by Tad Lispy

index 4516ab8..3b50647 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -114,8 +114,6 @@ pub struct ConstructionOrder {
=#[derive(Component)]
=pub struct Building;
=
-const MODEL_PATH: &str = "large_buildingB.glb#Scene0";
-
=/// Issue a construction order, for the record and effect
=fn order_construction(
=    mut build_events: EventWriter<ConstructionOrder>,

Use iterators more concisely

On by Tad Lispy

Thanks to Clippy.

index 3b50647..54792ee 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -127,10 +127,8 @@ fn order_construction(
=    for class in models.classes() {
=        info!("Let's build a {class}");
=        // Find parcel that matches this class
-        let Some((entity, _, transform)) = parcels
-            .iter()
-            .filter(|(_, parcel, _)| parcel.class == class)
-            .next()
+        let Some((entity, _, transform)) =
+            parcels.iter().find(|(_, parcel, _)| parcel.class == class)
=        else {
=            warn!("Can't build {class}. No suitable parcels.");
=            continue;
index 9709fc1..170e104 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -57,11 +57,7 @@ fn setup_parcels(
=    let parcels: Vec<(Entity, String, Transform)> = query
=        .iter(&scene.world)
=        .filter_map(|(entity, name, transform)| {
-            if let Some(class) = building_class(name) {
-                Some((entity, class, transform.clone()))
-            } else {
-                None
-            }
+            building_class(name).map(|class| (entity, class, *transform))
=        })
=        .collect_vec();
=

Make timescale into a resource

On by Tad Lispy

Can be changed at runtime via world inspector. In the future we can make a custom UI for this.

index 3ceb389..36df5b5 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -2,17 +2,16 @@ use crate::day_month::{DayMonth, HOUR, MINUTE};
=use crate::{GameState, BEGINNING, DURATION};
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
-
-/// Day-months per one second of real time during exploration
-const SCALE: f32 = 1.0 / 10.0;
-/// Day-months per one second of real time during simulation
-const SIMULATION_SCALE: f32 = 2.0;
+use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
+use bevy_inspector_egui::prelude::*;
=
=pub struct DatePlugin;
=
=impl Plugin for DatePlugin {
=    fn build(&self, app: &mut App) {
=        app.insert_resource(Date(DayMonth::new(BEGINNING)))
+            .register_type::<Timescale>()
+            .init_resource::<Timescale>()
=            .add_event::<NewMonth>()
=            .add_systems(Startup, setup_date_display)
=            .add_systems(Startup, register_date_systems)
@@ -23,6 +22,27 @@ impl Plugin for DatePlugin {
=#[derive(Resource)]
=pub struct Date(pub DayMonth);
=
+#[derive(Resource, Reflect, InspectorOptions)]
+#[reflect(Resource, InspectorOptions)]
+// TODO: It's hard to control small fractions. Make the scale logarithmic?
+struct Timescale {
+    /// Day-months per one second of real time during exploration
+    #[inspector(min = 0.0, max = 2.0, display = NumberDisplay::Slider)]
+    scale: f32,
+    /// Day-months per one second of real time during simulation
+    #[inspector(min = 0.0, max = 2.0, display = NumberDisplay::Slider)]
+    simulation_scale: f32,
+}
+
+impl Default for Timescale {
+    fn default() -> Self {
+        Self {
+            scale: 1.0 / 10.0,
+            simulation_scale: 2.0,
+        }
+    }
+}
+
=#[derive(Event)]
=pub struct NewMonth;
=
@@ -31,13 +51,14 @@ fn advance_date(
=    mut date: ResMut<Date>,
=    mut events: EventWriter<NewMonth>,
=    game_state: Res<State<GameState>>,
+    timescale: Res<Timescale>,
=) {
=    // Do not go much over the duration, but gently slow down after
=    const ENDTIME: f32 = 9.0 * HOUR + 32.0 * MINUTE;
=    let mut scale = if game_state.get() == &GameState::Simulate {
-        SIMULATION_SCALE
+        timescale.simulation_scale
=    } else {
-        SCALE
+        timescale.scale
=    };
=
=    if date.0.year() >= (BEGINNING + DURATION) {

Drop another unused constant

On by Tad Lispy

index 336504e..cc8e080 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -2,7 +2,6 @@ use std::fmt::Display;
=
=pub const HOUR: f32 = 1.0 / 24.0;
=pub const MINUTE: f32 = HOUR / 60.0;
-pub const MINUTES_PER_DAYMONTH: f32 = (60 * 24) as f32;
=
=#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
=pub enum Month {

Debugging asset crashing the app (wip).

On by pedrolinux

index 19d0f64..3d62128 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differ
index 4607724..746e372 100644
Binary files a/art/districts.blend and b/art/districts.blend differ
index 724844e..353c3d0 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differ
index 52a9104..b49a306 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differ

reverted stash commit - buildings with models in parcels.

On by pedrolinux

index 00c1e1b..c4784ac 100644
Binary files a/art/district_180x300_b.blend and b/art/district_180x300_b.blend differ

Re-enable snapshots and rollback for buildings

On by Tad Lispy

There is a new BuildingDescription type that is used by ConstructionOrder event and buildings::Snapshot.

index 54792ee..f64ac8a 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,6 +1,7 @@
=use crate::date::NewMonth;
=use crate::districts::Parcel;
=use crate::{GameState, PreloadedAssets};
+use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
=use bevy::utils::HashMap;
@@ -16,19 +17,39 @@ impl Plugin for BuildingsPlugin {
=            .init_resource::<BuildingModels>()
=            .register_type::<BuildingModels>()
=            .add_systems(Startup, setup_assets)
+            .add_systems(Startup, register_buildings_systems)
=            .add_systems(OnEnter(GameState::Simulate), setup_building_models)
=            .add_systems(
=                Update,
=                order_construction
=                    .run_if(on_event::<NewMonth>().and_then(in_state(GameState::Simulate))),
=            )
-            .add_systems(Update, spawn_buildings);
+            .add_systems(Update, receive_orders);
=    }
=}
=
=#[derive(Resource)]
=struct BuildingAssets(Handle<Gltf>);
=
+#[derive(Resource, Debug)]
+pub struct BuildingsSystems {
+    pub take_snapshot: SystemId<(), Snapshot>,
+    pub rollback: SystemId<Snapshot>,
+    pub construct_building: SystemId<BuildingDescription>,
+}
+
+fn register_buildings_systems(world: &mut World) {
+    let take_snapshot = world.register_system(take_snapshot);
+    let rollback = world.register_system(rollback);
+    let construct_building = world.register_system(construct_building);
+
+    world.insert_resource(BuildingsSystems {
+        take_snapshot,
+        rollback,
+        construct_building,
+    });
+}
+
=fn setup_assets(
=    assets: Res<AssetServer>,
=    mut commands: Commands,
@@ -105,14 +126,19 @@ fn setup_building_models(
=///
=/// It will be stored in the history, and will result in spawning a new building.
=#[derive(Event, Clone, Debug)]
-pub struct ConstructionOrder {
+pub struct ConstructionOrder(pub BuildingDescription);
+
+#[derive(Clone, Debug)]
+pub struct BuildingDescription {
=    pub transform: Transform,
=    pub model: String,
=}
=
=/// A tag for building entities
=#[derive(Component)]
-pub struct Building;
+pub struct Building {
+    model: String,
+}
=
=/// Issue a construction order, for the record and effect
=fn order_construction(
@@ -140,58 +166,71 @@ fn order_construction(
=        commands.entity(entity).despawn();
=
=        // Order the construction
-        build_events.send(ConstructionOrder {
+        let description = BuildingDescription {
=            transform: transform.to_owned().into(),
=            model: model.to_string(),
-        });
+        };
+        build_events.send(ConstructionOrder(description));
=    }
=}
=
=/// Implement the construction orders
-fn spawn_buildings(
+fn receive_orders(
=    mut commands: Commands,
=    mut construction_orders: EventReader<ConstructionOrder>,
-    models: Res<BuildingModels>,
+    systems: Res<BuildingsSystems>,
=) {
-    for order in construction_orders.read() {
-        let Vec3 { x, z, .. } = order.transform.translation;
-        let model = models.0.get(&order.model).unwrap();
-
-        info!("Spawning a new building at ({x:.2}, {z:.2})!");
-
-        commands
-            .spawn(SceneBundle {
-                scene: model.scene.clone(),
-                transform: order.transform,
-                ..default()
-            })
-            .insert(Building)
-            .insert(Name::new(order.model.clone()));
+    for ConstructionOrder(description) in construction_orders.read() {
+        commands.run_system_with_input(systems.construct_building, description.clone());
=    }
=}
=
-// fn rollback(
-//     mut requests: EventReader<TimeTravelRequest>,
-//     buildings: Query<Entity, With<Building>>,
-//     assets: ResMut<AssetServer>,
-//     mut commands: Commands,
-// ) {
-//     for TimeTravelRequest(snapshot) in requests.read() {
-//         for entity in buildings.iter() {
-//             commands.entity(entity).despawn_recursive();
-//         }
-
-//         for transform in snapshot.clone().buildings {
-//             // TODO: DRY with spawn_buildings
-//             let model = assets.load(MODEL_PATH);
-
-//             commands
-//                 .spawn(SceneBundle {
-//                     scene: model,
-//                     transform: transform.clone(),
-//                     ..default()
-//                 })
-//                 .insert(Building);
-//         }
-//     }
-// }
+/// Implement the construction orders
+fn construct_building(
+    In(description): In<BuildingDescription>,
+    mut commands: Commands,
+    models: Res<BuildingModels>,
+) {
+    let Vec3 { x, z, .. } = description.transform.translation;
+    let model = models.0.get(&description.model).unwrap();
+
+    info!("Spawning a new building at ({x:.2}, {z:.2})!");
+
+    commands
+        .spawn(SceneBundle {
+            scene: model.scene.clone(),
+            transform: description.transform,
+            ..default()
+        })
+        .insert(Building {
+            model: description.model.clone(),
+        })
+        .insert(Name::new(description.model));
+}
+
+pub type Snapshot = Vec<BuildingDescription>;
+
+pub fn take_snapshot(buildings: Query<(&Building, &Transform)>) -> Snapshot {
+    buildings
+        .iter()
+        .map(|(Building { model }, transform)| BuildingDescription {
+            model: model.to_string(),
+            transform: *transform,
+        })
+        .collect()
+}
+
+fn rollback(
+    In(snapshot): In<Snapshot>,
+    buildings: Query<Entity, With<Building>>,
+    mut commands: Commands,
+    systems: Res<BuildingsSystems>,
+) {
+    for entity in buildings.iter() {
+        commands.entity(entity).despawn_recursive();
+    }
+
+    for order in snapshot {
+        commands.run_system_with_input(systems.construct_building, order.to_owned());
+    }
+}
index 9e220bc..fd086e8 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,5 +1,5 @@
-use crate::buildings;
-use crate::buildings::ConstructionOrder;
+use crate::buildings::{self, BuildingDescription};
+use crate::buildings::{BuildingsSystems, ConstructionOrder};
=use crate::date::{Date, DateSystems, NewMonth};
=use crate::day_month::{DayMonth, Month};
=use crate::districts::{DistrictsSystems, NewDistrictEstablished};
@@ -61,7 +61,10 @@ pub struct EventLogEntry {
=impl Display for HistoricalEvent {
=    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
=        match self {
-            HistoricalEvent::ConstructionOrder(ConstructionOrder { transform, model }) => {
+            HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
+                transform,
+                model,
+            })) => {
=                let Vec3 { x, z, .. } = transform.translation;
=                write!(
=                    f,
@@ -103,7 +106,7 @@ fn register_historical_events(
=#[derive(Clone, Debug)]
=pub struct Snapshot {
=    pub date: DayMonth,
-    pub buildings: Vec<Transform>,
+    pub buildings: buildings::Snapshot,
=    pub districts: districts::Snapshot,
=    pub roads: roads::Snapshot,
=}
@@ -125,8 +128,11 @@ fn take_snapshot(world: &mut World) {
=            world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.take_snapshot);
=        world.run_system(system).unwrap()
=    };
-    let buildings = Vec::default();
-    // let buildings = buildings.iter().map(|transform| *transform).collect();
+    let buildings = {
+        let system =
+            world.resource_scope(|_, systems: Mut<BuildingsSystems>| systems.take_snapshot);
+        world.run_system(system).unwrap()
+    };
=
=    let timestamp: Timestamp = date.into();
=
@@ -158,13 +164,18 @@ fn rollback(In(snapshot): In<Snapshot>, world: &mut World) {
=        let system = world.resource_scope(|_, systems: Mut<RoadsSystems>| systems.rollback);
=        world.run_system_with_input(system, snapshot.roads).unwrap();
=    }
-
=    {
=        let system = world.resource_scope(|_, systems: Mut<DistrictsSystems>| systems.rollback);
=        world
=            .run_system_with_input(system, snapshot.districts)
=            .unwrap();
=    }
+    {
+        let system = world.resource_scope(|_, systems: Mut<BuildingsSystems>| systems.rollback);
+        world
+            .run_system_with_input(system, snapshot.buildings)
+            .unwrap();
+    }
=
=    let events = world.resource_scope(|_, history: Mut<History>| history.events.clone());
=
index 119e9aa..7bfe650 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,6 +1,6 @@
-use crate::buildings::ConstructionOrder;
+use crate::buildings::{BuildingDescription, ConstructionOrder};
=use crate::districts::NewDistrictEstablished;
-use crate::history::History;
+use crate::history::{HistoricalEvent, History};
=use bevy::math::Vec3;
=use std::fmt::Display;
=
@@ -32,10 +32,10 @@ impl Display for History {
=            let second = 00;
=
=            match event.event.clone() {
-                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder {
+                HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
=                    transform,
=                    model,
-                }) => {
+                })) => {
=                    let Vec3 { x, z, .. } = transform.translation;
=
=                    writeln!(f, "Insert into buildings (")?;
@@ -51,9 +51,7 @@ impl Display for History {
=                    writeln!(f, "   ({x:.3}, {z:.3})")?;
=                    writeln!(f, ");")?;
=                }
-                crate::history::HistoricalEvent::NewDistrictEstablished(
-                    NewDistrictEstablished(district),
-                ) => {
+                HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
=                    writeln!(f, "Insert into districts (")?;
=                    writeln!(f, "   date,")?;
=                    writeln!(f, "   coordinates")?;

Fix program crashing on some building data

On by Tad Lispy

It turned out that meshes nested in building objects sometimes accidentally got named in a way that would cause them to be taken as parcels.

The solution I came up with is to find root entity of the scene and only consider it's direct children to be possible parcels. This way we avoid nested entities like meshes being mistaken for parcels, if they name accidentally match the building class pattern.

It doesn't seem like a very clean solution, but it has an advantage of not introducing any manual labor, like tagging parcels with custom properties or giving them weird names.

index 170e104..e699c00 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -53,15 +53,35 @@ fn setup_parcels(
=    let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
=
=    let scene = scenes.get_mut(scene_handle).unwrap();
-    let mut query = scene.world.query::<(Entity, &Name, &Transform)>();
+
+    let root = scene
+        .world
+        .query_filtered::<Entity, Without<Parent>>()
+        .iter(&scene.world)
+        .next()
+        .unwrap();
+
+    let mut query = scene.world.query::<(Entity, &Name, &Transform, &Parent)>();
=    let parcels: Vec<(Entity, String, Transform)> = query
=        .iter(&scene.world)
-        .filter_map(|(entity, name, transform)| {
+        .filter_map(|(entity, name, transform, parent)| {
+            // Only consider direct first generation to avoid nested entities
+            // with names matching the pattern being taken for parcels
+            if parent.get() != root {
+                info!("Skipping indirect descendant {name}");
+                return None;
+            };
+            // Check if the name matches pattern
=            building_class(name).map(|class| (entity, class, *transform))
=        })
=        .collect_vec();
=
=    for (entity, class, transform) in parcels {
+        info!("Processing parcel {entity:?}, class {class}");
+
+        for component in scene.world.inspect_entity(entity) {
+            info!("- {name}", name = component.name())
+        }
=        scene
=            .world
=            .spawn(SpatialBundle {
@@ -70,7 +90,11 @@ fn setup_parcels(
=                ..default()
=            })
=            .insert(Parcel { class });
-        scene.world.entity_mut(entity).despawn_recursive();
+        if let Some(entity) = scene.world.get_entity_mut(entity) {
+            entity.despawn_recursive();
+        } else {
+            warn!("This entity does not exist!");
+        };
=    }
=}
=

Re-adding districts and new buildings.

On by pedrolinux

index 3d62128..4394302 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differ
index 746e372..f21b365 100644
Binary files a/art/districts.blend and b/art/districts.blend differ
index 353c3d0..2cd2b93 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differ
index b49a306..6954a33 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differ

Increased the amount of building orders to 5 per class.

On by pedrolinux

index f64ac8a..51f49de 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -152,25 +152,22 @@ fn order_construction(
=
=    for class in models.classes() {
=        info!("Let's build a {class}");
-        // Find parcel that matches this class
-        let Some((entity, _, transform)) =
-            parcels.iter().find(|(_, parcel, _)| parcel.class == class)
-        else {
-            warn!("Can't build {class}. No suitable parcels.");
-            continue;
-        };
-        let models = &models.get_class(&class);
-        let model = models.keys().choose(&mut rand::thread_rng()).unwrap();
-
-        // Remove parcel
-        commands.entity(entity).despawn();
-
-        // Order the construction
-        let description = BuildingDescription {
-            transform: transform.to_owned().into(),
-            model: model.to_string(),
-        };
-        build_events.send(ConstructionOrder(description));
+
+        // Filter parcel that matches this class
+        for (entity, _, transform) in parcels.iter().filter(|(_, parcel, _)| parcel.class == class).take(5) {
+            let models = &models.get_class(&class);
+            let model = models.keys().choose(&mut rand::thread_rng()).unwrap();
+    
+            // Remove parcel
+            commands.entity(entity).despawn();
+    
+            // Order the construction
+            let description = BuildingDescription {
+                transform: transform.to_owned().into(),
+                model: model.to_string(),
+            };
+            build_events.send(ConstructionOrder(description));
+        }
=    }
=}
=

Rename a variable

On by Tad Lispy

The name was copy-pasted and didn't make sense.

index e699c00..9dd20fd 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -178,9 +178,9 @@ impl District {
=
=fn establish_new_districts(
=    mut new_districts: EventWriter<NewDistrictEstablished>,
-    buildings: Query<&District>,
+    districts: Query<&District>,
=) {
-    let count = buildings.into_iter().count();
+    let count = districts.into_iter().count();
=
=    const ROWS: usize = 5;
=    const WIDTH: usize = 18;

Make the logs less verbose

On by Tad Lispy

Some messages were useful while developing. Now they are just noise.

index 9dd20fd..3baae61 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -68,7 +68,7 @@ fn setup_parcels(
=            // Only consider direct first generation to avoid nested entities
=            // with names matching the pattern being taken for parcels
=            if parent.get() != root {
-                info!("Skipping indirect descendant {name}");
+                debug!("Skipping indirect descendant {name}");
=                return None;
=            };
=            // Check if the name matches pattern
@@ -79,9 +79,6 @@ fn setup_parcels(
=    for (entity, class, transform) in parcels {
=        info!("Processing parcel {entity:?}, class {class}");
=
-        for component in scene.world.inspect_entity(entity) {
-            info!("- {name}", name = component.name())
-        }
=        scene
=            .world
=            .spawn(SpatialBundle {
index 64921ba..0196fb4 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -76,7 +76,6 @@ fn register_road_systems(world: &mut World) {
=    let construct_road_section = world.register_system(construct_road_section);
=    let implement_road_plan = world.register_system(implement_road_plan);
=
-    info!("Setting up roads systems");
=    world.insert_resource(RoadsSystems {
=        take_snapshot,
=        rollback,

WIP: Intelligent districts placement

On by Tad Lispy

Initially the districts were placed in a grid, 6 in a row. Now they are placed at road intersections, as close to the center as possible without overlapping with other districts.

index 39f24b9..a4fec75 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -1,9 +1,8 @@
=use derive_more::{AsRef, From, Into};
+use bevy::math::{Vec2, Vec3};
=use std::fmt::Display;
=use std::ops::{AddAssign, SubAssign};
=
-use bevy::math::Vec3;
-
=/// First element is longitude from west to east, second is latitude from north to south
=// TODO: Specify Latitude and Longitude types for safety
=#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, AsRef)]
@@ -56,12 +55,21 @@ impl Coordinates {
=    }
=}
=
+impl From<&Coordinates> for Vec2 {
+    fn from(coordinates: &Coordinates) -> Self {
+        let x: i32 = coordinates.longitude().into();
+        let y: i32 = coordinates.latitude().into();
+
+        Self::new((x * 10) as f32, (y * 10) as f32)
+    }
+}
+
=impl From<&Coordinates> for Vec3 {
=    fn from(coordinates: &Coordinates) -> Self {
=        let x: i32 = coordinates.longitude().into();
=        let z: i32 = coordinates.latitude().into();
=
-        Vec3::new((x * 10) as f32, 0.0, (z * 10) as f32)
+        Self::new((x * 10) as f32, 0.0, (z * 10) as f32)
=    }
=}
=
index 3baae61..7f1e3dc 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -8,6 +8,8 @@ use bevy::gltf::Gltf;
=use bevy::prelude::*;
=use itertools::Itertools;
=use std::borrow::Borrow;
+use std::iter;
+use std::ops::Not;
=
=pub struct DistrictsPlugin;
=
@@ -111,10 +113,20 @@ pub struct NewDistrictEstablished(pub District);
=#[derive(Component, Debug, Clone, Copy)]
=pub struct District {
=    pub origin: Coordinates,
+    // TODO: Consider storing width and length instead, so origin is always the north-east corner
=    pub extent: Coordinates,
=}
=
=impl District {
+    pub fn new(origin: Coordinates, length: usize, width: usize) -> Self {
+        let extent = *origin
+            .clone()
+            .shift(length as i32, &Direction::East)
+            .shift(width as i32, &Direction::South);
+
+        Self { origin, extent }
+    }
+
=    pub fn plan_border_roads(&self) -> RoadPlan {
=        RoadPlan::default()
=            .add_road(&self.north_west(), &Direction::East, self.length())
@@ -124,6 +136,10 @@ impl District {
=            .to_owned()
=    }
=
+    fn overlaps(&self, other: &Self) -> bool {
+        Rect::from(self).intersect(other.into()).is_empty().not()
+    }
+
=    // Edges
=
=    fn south(&self) -> Latitude {
@@ -144,6 +160,15 @@ impl District {
=
=    // Corners
=
+    fn corners(&self) -> [Coordinates; 4] {
+        [
+            self.north_east(),
+            self.south_east(),
+            self.south_west(),
+            self.north_west(),
+        ]
+    }
+
=    fn north_east(&self) -> Coordinates {
=        Coordinates::new(self.east(), self.north())
=    }
@@ -173,30 +198,60 @@ impl District {
=    }
=}
=
+impl From<&District> for Rect {
+    fn from(value: &District) -> Self {
+        Rect::from_corners(
+            value.north_west().borrow().into(),
+            value.south_east().borrow().into(),
+        )
+    }
+}
+
=fn establish_new_districts(
-    mut new_districts: EventWriter<NewDistrictEstablished>,
=    districts: Query<&District>,
+    mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
-    let count = districts.into_iter().count();
-
-    const ROWS: usize = 5;
+    // TODO: Read from the asset (scene)
=    const WIDTH: usize = 18;
-    const BREDTH: usize = 30;
-
-    let x = (count.rem_euclid(ROWS) * WIDTH) as i32;
-    let y = (count / ROWS * BREDTH) as i32;
-
-    let origin = Coordinates::new(Longitude::from(x), Latitude::from(y));
-    let extent = origin
-        .clone()
-        .shift(18, &Direction::East)
-        .shift(30, &Direction::South)
-        .to_owned();
-
-    let district = District { origin, extent };
-    info!("New district established: {district:?}");
+    const LENGTH: usize = 30;
+
+    let corners = districts
+        .iter()
+        .flat_map(|district| district.corners().into_iter())
+        .chain(iter::once(Coordinates::default()))
+        .unique();
+
+    let candidates = corners.filter_map(|corner| {
+        info!("Trying corner at {corner}");
+
+        // TODO: Also try rotated and flipped variants
+
+        let candidate = District::new(corner, WIDTH, LENGTH);
+        if districts
+            .iter()
+            .any(|existing| existing.overlaps(&candidate))
+        {
+            info!("No can do. Overlaps with existing");
+            None
+        } else {
+            info!("That's a prime spot!");
+            Some(candidate)
+        }
+    });
=
-    new_districts.send(NewDistrictEstablished(district));
+    // Chose one closest to the center
+    let Some(selected) = candidates.min_by(|a, b| {
+        Rect::from(a)
+            .center()
+            .length()
+            .total_cmp(&Rect::from(b).center().length())
+    }) else {
+        info!("Can't find a suitable spot for a new district!");
+        return;
+    };
+
+    info!("New district established: {selected:?}");
+    new_districts.send(NewDistrictEstablished(selected));
=}
=
=fn spawn_new_districts(

Refactor District type to store width and length

On by Tad Lispy

Instead of two sets of coordinates. Both dimensions are unsigned. This way it's impossible to construct a district which has origin anywhere else than in it's north-west corner. It's nice to have this invariant.

index a4fec75..6888df0 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -1,7 +1,7 @@
-use derive_more::{AsRef, From, Into};
=use bevy::math::{Vec2, Vec3};
+use derive_more::{AsRef, From, Into};
=use std::fmt::Display;
-use std::ops::{AddAssign, SubAssign};
+use std::ops::{Add, AddAssign, SubAssign};
=
=/// First element is longitude from west to east, second is latitude from north to south
=// TODO: Specify Latitude and Longitude types for safety
@@ -75,6 +75,13 @@ impl From<&Coordinates> for Vec3 {
=
=// TODO: DRY on Latitude and Longitude. Maybe create a derivable trait Coordinate?
=
+impl Add<i32> for Latitude {
+    type Output = Self;
+    fn add(self, rhs: i32) -> Self::Output {
+        Self(self.0 + rhs)
+    }
+}
+
=impl AddAssign<i32> for Latitude {
=    fn add_assign(&mut self, rhs: i32) {
=        self.0 += rhs;
@@ -87,6 +94,13 @@ impl SubAssign<i32> for Latitude {
=    }
=}
=
+impl Add<i32> for Longitude {
+    type Output = Self;
+    fn add(self, rhs: i32) -> Self::Output {
+        Self(self.0 + rhs)
+    }
+}
+
=impl AddAssign<i32> for Longitude {
=    fn add_assign(&mut self, rhs: i32) {
=        self.0 += rhs;
index 7f1e3dc..02cf999 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -112,19 +112,21 @@ pub struct NewDistrictEstablished(pub District);
=/// A tag for district entities
=#[derive(Component, Debug, Clone, Copy)]
=pub struct District {
+    /// Coordinates of the north-west corner
=    pub origin: Coordinates,
-    // TODO: Consider storing width and length instead, so origin is always the north-east corner
-    pub extent: Coordinates,
+    /// The size longitudinal (x-axis aligned) dimension
+    pub length: u32,
+    /// The size latitudinal (z-axis aligned) dimension
+    pub width: u32,
=}
=
=impl District {
-    pub fn new(origin: Coordinates, length: usize, width: usize) -> Self {
-        let extent = *origin
-            .clone()
-            .shift(length as i32, &Direction::East)
-            .shift(width as i32, &Direction::South);
-
-        Self { origin, extent }
+    pub fn new(origin: Coordinates, length: u32, width: u32) -> Self {
+        Self {
+            origin,
+            length,
+            width,
+        }
=    }
=
=    pub fn plan_border_roads(&self) -> RoadPlan {
@@ -143,19 +145,19 @@ impl District {
=    // Edges
=
=    fn south(&self) -> Latitude {
-        self.origin.latitude().max(self.extent.latitude())
+        self.origin.latitude() + (self.width as i32)
=    }
=
=    fn east(&self) -> Longitude {
-        self.origin.longitude().max(self.extent.longitude())
+        self.origin.longitude() + (self.length as i32)
=    }
=
=    fn north(&self) -> Latitude {
-        self.origin.latitude().min(self.extent.latitude())
+        self.origin.latitude()
=    }
=
=    fn west(&self) -> Longitude {
-        self.origin.longitude().min(self.extent.longitude())
+        self.origin.longitude()
=    }
=
=    // Corners
@@ -170,19 +172,29 @@ impl District {
=    }
=
=    fn north_east(&self) -> Coordinates {
-        Coordinates::new(self.east(), self.north())
+        *self
+            .origin
+            .clone()
+            .shift(self.length as i32, &Direction::East)
=    }
=
=    fn north_west(&self) -> Coordinates {
-        Coordinates::new(self.west(), self.north())
+        self.origin
=    }
=
=    fn south_east(&self) -> Coordinates {
-        Coordinates::new(self.east(), self.south())
+        *self
+            .origin
+            .clone()
+            .shift(self.width as i32, &Direction::South)
+            .shift(self.length as i32, &Direction::East)
=    }
=
=    fn south_west(&self) -> Coordinates {
-        Coordinates::new(self.west(), self.south())
+        *self
+            .origin
+            .clone()
+            .shift(self.width as i32, &Direction::South)
=    }
=
=    // Dimensions
@@ -212,8 +224,8 @@ fn establish_new_districts(
=    mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
=    // TODO: Read from the asset (scene)
-    const WIDTH: usize = 18;
-    const LENGTH: usize = 30;
+    const WIDTH: u32 = 18;
+    const LENGTH: u32 = 30;
=
=    let corners = districts
=        .iter()
@@ -344,9 +356,11 @@ mod district_tests {
=
=    #[test]
=    fn measurements_test() {
-        let origin = Coordinates::new(Longitude::new(-3), Latitude::new(-2));
-        let extent = Coordinates::new(Longitude::new(10), Latitude::new(5));
-        let district = District { origin, extent };
+        let district = District::new(
+            Coordinates::new(Longitude::new(-3), Latitude::new(-2)),
+            13,
+            7,
+        );
=
=        assert_eq!(district.east(), Longitude::new(10));
=        assert_eq!(district.west(), Longitude::new(-3));

Switch names of the width and length district constants

On by Tad Lispy

They were used incorrectly.

index 02cf999..b7bc41e 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -224,8 +224,8 @@ fn establish_new_districts(
=    mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
=    // TODO: Read from the asset (scene)
-    const WIDTH: u32 = 18;
-    const LENGTH: u32 = 30;
+    const LENGTH: u32 = 18;
+    const WIDTH: u32 = 30;
=
=    let corners = districts
=        .iter()
@@ -238,7 +238,7 @@ fn establish_new_districts(
=
=        // TODO: Also try rotated and flipped variants
=
-        let candidate = District::new(corner, WIDTH, LENGTH);
+        let candidate = District::new(corner, LENGTH, WIDTH);
=        if districts
=            .iter()
=            .any(|existing| existing.overlaps(&candidate))

All sides of an intersections will be considered

On by Tad Lispy

...when placing new districts, not only the south-eastern corner of the intersection.

index b7bc41e..3941835 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -129,6 +129,11 @@ impl District {
=        }
=    }
=
+    pub fn shift(&mut self, distance: i32, direction: &Direction) -> &mut Self {
+        self.origin.shift(distance, direction);
+        self
+    }
+
=    pub fn plan_border_roads(&self) -> RoadPlan {
=        RoadPlan::default()
=            .add_road(&self.north_west(), &Direction::East, self.length())
@@ -233,23 +238,25 @@ fn establish_new_districts(
=        .chain(iter::once(Coordinates::default()))
=        .unique();
=
-    let candidates = corners.filter_map(|corner| {
-        info!("Trying corner at {corner}");
-
-        // TODO: Also try rotated and flipped variants
+    let candidates = corners
+        .flat_map(|corner| {
+            let a = District::new(corner, LENGTH, WIDTH);
+            let b = *a.clone().shift(a.width as i32, &Direction::North);
+            let c = *a
+                .clone()
+                .shift(a.width as i32, &Direction::North)
+                .shift(a.length as i32, &Direction::West);
+            let d = *a.clone().shift(a.length as i32, &Direction::West);
+
+            [a, b, c, d].into_iter()
+        })
+        .filter(|candidate| {
+            info!("Trying corner {candidate:?}");
=
-        let candidate = District::new(corner, LENGTH, WIDTH);
-        if districts
-            .iter()
-            .any(|existing| existing.overlaps(&candidate))
-        {
-            info!("No can do. Overlaps with existing");
-            None
-        } else {
-            info!("That's a prime spot!");
-            Some(candidate)
-        }
-    });
+            districts
+                .iter()
+                .any(|existing| existing.overlaps(candidate))
+        });
=
=    // Chose one closest to the center
=    let Some(selected) = candidates.min_by(|a, b| {

Fix districts not getting established

On by Tad Lispy

The filter was inverse :P

index 3941835..17e94a7 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -256,6 +256,7 @@ fn establish_new_districts(
=            districts
=                .iter()
=                .any(|existing| existing.overlaps(candidate))
+                .not()
=        });
=
=    // Chose one closest to the center

Implement new district rotation, randomness

On by Tad Lispy

New districts are spawned randomly rotated and translated around intersections.

At first I thought that there should be a weighted probability for their position, where districts would be more likely to be constructed closer to the center. But after running the simulation with standard distribution, I think it's looks more organic that way. Because districts are constructed around intersections, there is natural tendency for them to cluster.

index 17e94a7..1517563 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -7,9 +7,12 @@ use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
=use itertools::Itertools;
+use rand::seq::IteratorRandom;
+use rand::thread_rng;
=use std::borrow::Borrow;
+use std::f32::consts::FRAC_PI_2;
=use std::iter;
-use std::ops::Not;
+use std::ops::{AddAssign, Not};
=
=pub struct DistrictsPlugin;
=
@@ -118,6 +121,48 @@ pub struct District {
=    pub length: u32,
=    /// The size latitudinal (z-axis aligned) dimension
=    pub width: u32,
+    /// How is the district rotated compared to the model
+    rotation: Rotation,
+}
+
+#[derive(Component, Debug, Clone, Copy)]
+pub enum Rotation {
+    None = 0,
+    CW90,
+    CW180,
+    CW270,
+}
+
+impl AddAssign for Rotation {
+    fn add_assign(&mut self, rhs: Self) {
+        let current = *self as isize;
+        let increase = rhs as isize;
+        *self = Self::from(current + increase);
+    }
+}
+
+impl From<Rotation> for Quat {
+    fn from(value: Rotation) -> Self {
+        let angle = value as isize as f32 * FRAC_PI_2;
+
+        // TODO: Seems like Quat::from_axis_angle is counter-clockwise. Shall the Rotation be too?
+        Quat::from_axis_angle(Vec3::Y, -angle)
+    }
+}
+
+impl From<isize> for Rotation {
+    fn from(value: isize) -> Self {
+        let remainder = value.rem_euclid(4);
+        match remainder {
+            0 => Rotation::None,
+            1 => Rotation::CW90,
+            2 => Rotation::CW180,
+            3 => Rotation::CW270,
+            _ => {
+                panic!("The remainder of {value} modulo 4 = {remainder} (not in 0 - 3 range). WTF?")
+            }
+        }
+    }
=}
=
=impl District {
@@ -126,6 +171,7 @@ impl District {
=            origin,
=            length,
=            width,
+            rotation: Rotation::None,
=        }
=    }
=
@@ -134,6 +180,26 @@ impl District {
=        self
=    }
=
+    pub fn rotate(&mut self, rotation: Rotation) -> &mut Self {
+        let Self { length, width, .. } = *self;
+
+        match rotation {
+            Rotation::None => {}
+            Rotation::CW90 => {
+                self.length = width;
+                self.width = length;
+            }
+            Rotation::CW180 => {}
+            Rotation::CW270 => {
+                self.length = width;
+                self.width = length;
+            }
+        }
+
+        self.rotation += rotation;
+        self
+    }
+
=    pub fn plan_border_roads(&self) -> RoadPlan {
=        RoadPlan::default()
=            .add_road(&self.north_west(), &Direction::East, self.length())
@@ -147,6 +213,11 @@ impl District {
=        Rect::from(self).intersect(other.into()).is_empty().not()
=    }
=
+    pub fn center(&self) -> Vec3 {
+        let Vec2 { x, y } = Rect::from(self).center();
+        Vec3 { x, y: 0.0, z: y }
+    }
+
=    // Edges
=
=    fn south(&self) -> Latitude {
@@ -239,33 +310,48 @@ fn establish_new_districts(
=        .unique();
=
=    let candidates = corners
-        .flat_map(|corner| {
-            let a = District::new(corner, LENGTH, WIDTH);
-            let b = *a.clone().shift(a.width as i32, &Direction::North);
-            let c = *a
-                .clone()
-                .shift(a.width as i32, &Direction::North)
-                .shift(a.length as i32, &Direction::West);
-            let d = *a.clone().shift(a.length as i32, &Direction::West);
-
-            [a, b, c, d].into_iter()
+        .map(|corner| District::new(corner, LENGTH, WIDTH))
+        .flat_map(|candidate| {
+            [
+                *candidate.clone().rotate(Rotation::None),
+                *candidate.clone().rotate(Rotation::CW90),
+                *candidate.clone().rotate(Rotation::CW180),
+                *candidate.clone().rotate(Rotation::CW270),
+            ]
+            .into_iter()
+        })
+        .flat_map(|candidate| {
+            let District { length, width, .. } = candidate;
+            [
+                candidate,
+                *candidate.clone().shift(width as i32, &Direction::North),
+                *candidate
+                    .clone()
+                    .shift(width as i32, &Direction::North)
+                    .shift(length as i32, &Direction::West),
+                *candidate.clone().shift(length as i32, &Direction::West),
+            ]
+            .into_iter()
=        })
=        .filter(|candidate| {
=            info!("Trying corner {candidate:?}");
=
+            // TODO: Extract into Latitude::same_hemisphere method
+            if i32::from(candidate.east()) * i32::from(candidate.west()) < 0 {
+                info!("District would cross the latitudinal highway");
+                return false;
+            }
+            if i32::from(candidate.north()) * i32::from(candidate.south()) < 0 {
+                info!("District would cross the longitudinal highway");
+                return false;
+            }
+
=            districts
=                .iter()
=                .any(|existing| existing.overlaps(candidate))
=                .not()
=        });
-
-    // Chose one closest to the center
-    let Some(selected) = candidates.min_by(|a, b| {
-        Rect::from(a)
-            .center()
-            .length()
-            .total_cmp(&Rect::from(b).center().length())
-    }) else {
+    let Some(selected) = candidates.choose(&mut thread_rng()) else {
=        info!("Can't find a suitable spot for a new district!");
=        return;
=    };
@@ -274,6 +360,7 @@ fn establish_new_districts(
=    new_districts.send(NewDistrictEstablished(selected));
=}
=
+// TODO: Rename
=fn spawn_new_districts(
=    mut commands: Commands,
=    mut new_districts: EventReader<NewDistrictEstablished>,
@@ -339,7 +426,28 @@ fn construct_district(
=
=    // TODO: Do it for every scene
=    let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
-    let transform = Transform::from_translation(district.origin.borrow().into());
+
+    // Rotate and translate the district model
+    // TODO: Move the transform logic below to a District::apply_transform method or something like that
+    let alignment: Transform = match district.rotation {
+        Rotation::None => Transform::default(),
+        Rotation::CW90 => Transform::from_translation(Vec3 {
+            z: (district.length * 10) as f32 * -1.0,
+            ..default()
+        }),
+        Rotation::CW180 => Transform::from_translation(Vec3 {
+            x: (district.length * 10) as f32 * -1.0,
+            z: (district.width * 10) as f32 * -1.0,
+            ..default()
+        }),
+        Rotation::CW270 => Transform::from_translation(Vec3 {
+            x: (district.width * 10) as f32 * -1.0,
+            ..default()
+        }),
+    };
+    let transform = Transform::from_translation(district.origin.borrow().into())
+        .with_rotation(district.rotation.into())
+        .mul_transform(alignment);
=
=    commands
=        .spawn(SceneBundle {
@@ -394,6 +502,36 @@ mod district_tests {
=        );
=    }
=
+    #[test]
+    fn into_rect() {
+        let district = District::new(Coordinates::default(), 10, 20);
+        let rect = Rect::from(&district);
+        assert_eq!(rect.min, Vec2::ZERO);
+        assert_eq!(rect.max, Vec2::new(100.0, 200.0));
+
+        let district = District::new(
+            Coordinates::new(Longitude::new(10), Latitude::new(5)),
+            10,
+            20,
+        );
+        let rect = Rect::from(&district);
+        assert_eq!(rect.min, Vec2::new(100.0, 50.0));
+        assert_eq!(rect.max, Vec2::new(200.0, 250.0));
+    }
+
+    #[test]
+    fn center() {
+        let district = District::new(Coordinates::default(), 10, 20);
+        assert_eq!(district.center(), Vec3::new(50.0, 0.0, 100.0));
+
+        let district = District::new(
+            Coordinates::new(Longitude::new(10), Latitude::new(5)),
+            10,
+            15,
+        );
+        assert_eq!(district.center(), Vec3::new(150.0, 0.0, 125.0));
+    }
+
=    #[test]
=    fn overlapping() {
=        // Separate
index d051364..88260a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -28,7 +28,7 @@ use roads::RoadsPlugin;
=use sun::SunPlugin;
=
=const BEGINNING: i32 = 1860;
-const DURATION: i32 = 1;
+const DURATION: i32 = 4;
=
=fn main() {
=    App::new()

Prevent districts from going beyond the landmass

On by Tad Lispy

index 1517563..6526a06 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -2,7 +2,7 @@ use crate::buildings::building_class;
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
=use crate::roads::{RoadPlan, RoadsSystems};
-use crate::{GameState, PreloadedAssets};
+use crate::{GameState, PreloadedAssets, Settings};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -297,6 +297,7 @@ impl From<&District> for Rect {
=
=fn establish_new_districts(
=    districts: Query<&District>,
+    settings: Res<Settings>,
=    mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
=    // TODO: Read from the asset (scene)
@@ -346,6 +347,13 @@ fn establish_new_districts(
=                return false;
=            }
=
+            let outer_buffer = ((candidate.length + candidate.width) * 10) as f32;
+            let max_distance = settings.land_radius - outer_buffer;
+            if candidate.center().length() > max_distance {
+                info!("District would be too close to the edge.");
+                return false;
+            }
+
=            districts
=                .iter()
=                .any(|existing| existing.overlaps(candidate))