Week 10 of 2024

Development log of Otterhide

38 items
  1. Added first neighbourhood model
  2. Move Timestamp logic from day_month to history
  3. Set simulation speed independent from exploration
  4. Set duration to 1 year
  5. WIP: Replay events in explore state
  6. Scale everything to Blender model of neighborhood
  7. Fix some events not being re-played
  8. Update dependencies
  9. Implement the export function that prints history
  10. Increase the land size. Control it via a resource
  11. Separate export logic to own module
  12. Apply custom formatting for export string
  13. Fix neighborhood meshes flickering
  14. Added road around and increased base thickness
  15. Write actual SQL string when exporting
  16. Fix SQL export: always return a value
  17. Added roads and initial house spots. Updated outer road side
  18. Scale the building model
  19. Separate buildings and districts logic
  20. Added 16 scenes with road blocks with naming 'RoadWSEN' (cardinal initials, West, South, East, North)
  21. Filled center of road blocks of curves and intersections with asphalt color
  22. Changed scene name to 000 to appear in first place
  23. Write some logic and tests regarding roads
  24. Separate road tiles to own .blend and .glb files
  25. Implement roads combination system
  26. Add simple camera controls
  27. Reintroduce districts, now with coordinates system
  28. Add Clippy to Rust toolchain
  29. Create a check goal in the Makefile
  30. Fix Coordinates::shift method
  31. When creating districts, create roads around them
  32. Introduce Latitude and Longitude types
  33. Fix the district model path
  34. Finished first district with building zones and roads. Added 4m in height to road blocks. Elevated base block and set origin at bottom.
  35. Added some kenney models to project folder in art/models. Added 5 scenes to buildings.blend with different building types. Exported to assets buildings.blend and neighbourhood_300x180_A.blend.
  36. Added small readme.md with just the instructions to run nix (not instructions on how to setup nix in the first place yet)
  37. Add f3d gltf viewer to the development environment
  38. The Coordinates constructor will take ownership

Added first neighbourhood model

On by pedrolinux

index e426355..1bc535a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
=/web/
=/build-image
=/.cargo-home/
+*.blend1
new file mode 100644
index 0000000..0081542
Binary files /dev/null and b/art/neighbourhood_300x180_A.blend differ
new file mode 100644
index 0000000..32a8739
Binary files /dev/null and b/assets/neighbourhood_300x180_A.glb differ

Move Timestamp logic from day_month to history

On by Tad Lispy

It is only used there and hopefully we can get rid of it soon.

index a3c7613..403e493 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -136,63 +136,6 @@ impl Display for DayMonth {
=    }
=}
=
-// TODO: Ditch the Timestamp type.
-// If we need keys at all, we can use String from a DayMonth. And maybe keys are not needed at all?
-/// Simplified DayMonth that can be used as a key in a hash map
-#[derive(PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)]
-pub struct Timestamp {
-    pub year: i32,
-    pub month: Month,
-    pub hour: i32,
-    pub minute: i32,
-}
-
-impl From<DayMonth> for Timestamp {
-    fn from(day_month: DayMonth) -> Self {
-        Self {
-            year: day_month.year(),
-            month: day_month.month(),
-            hour: day_month.hour(),
-            minute: day_month.minute() as i32,
-        }
-    }
-}
-
-impl From<Timestamp> for DayMonth {
-    fn from(timestamp: Timestamp) -> Self {
-        let minute = (timestamp.hour * 60 + timestamp.minute) as f32;
-
-        Self {
-            year: timestamp.year,
-            month: (timestamp.month as i32 - 1) as f32 + (minute / MINUTES_PER_DAYMONTH),
-        }
-    }
-}
-
-impl From<&Timestamp> for DayMonth {
-    fn from(timestamp: &Timestamp) -> Self {
-        let minute = (timestamp.hour * 60 + timestamp.minute) as f32;
-
-        Self {
-            year: timestamp.year,
-            month: (timestamp.month as i32 - 1) as f32 + (minute / MINUTES_PER_DAYMONTH),
-        }
-    }
-}
-
-impl Display for Timestamp {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{yyyy} {mmmm} {hh:02}:{mm:02}",
-            yyyy = self.year,
-            mmmm = self.month,
-            hh = self.hour,
-            mm = self.minute
-        )
-    }
-}
-
=#[cfg(test)]
=mod daymonth_tests {
=    use super::*;
index 0c01085..af6c46b 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,7 +1,7 @@
=use crate::buildings::ConstructionOrder;
=use crate::buildings::{self, Building};
=use crate::date::{Date, NewMonth};
-use crate::day_month::{DayMonth, Timestamp};
+use crate::day_month::{DayMonth, Month};
=use crate::GameState;
=use bevy::prelude::*;
=use bevy::utils::HashMap;
@@ -105,5 +105,38 @@ fn time_travel(
=) {
=    for _ in requests.read() {
=        game_state.set(GameState::Explore);
+
+// TODO: Ditch the Timestamp type.
+// If we need keys at all, we can use String from a DayMonth. And maybe keys are not needed at all?
+/// Simplified DayMonth that can be used as a key in a hash map
+#[derive(PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)]
+pub struct Timestamp {
+    pub year: i32,
+    pub month: Month,
+    pub hour: i32,
+    pub minute: i32,
+}
+
+impl From<DayMonth> for Timestamp {
+    fn from(day_month: DayMonth) -> Self {
+        Self {
+            year: day_month.year(),
+            month: day_month.month(),
+            hour: day_month.hour(),
+            minute: day_month.minute() as i32,
+        }
+    }
+}
+
+impl Display for Timestamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{yyyy} {mmmm} {hh:02}:{mm:02}",
+            yyyy = self.year,
+            mmmm = self.month,
+            hh = self.hour,
+            mm = self.minute
+        )
=    }
=}

Set simulation speed independent from exploration

On by Tad Lispy

So we can speed up one without affecting the other.

index 586e532..2bfbca3 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -3,8 +3,10 @@ use crate::history::TimeTravelRequest;
=use crate::{GameState, BEGINNING, DURATION};
=use bevy::prelude::*;
=
-/// Simulation day-months per one second of real time
-const SCALE: f32 = 1.0 / 60.0;
+/// 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;
=
=pub struct DatePlugin;
=
@@ -33,7 +35,7 @@ fn advance_date(
=    // 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 {
-        SCALE * 120.
+        SIMULATION_SCALE
=    } else {
=        SCALE
=    };

Set duration to 1 year

On by Tad Lispy

For quick iterations.

index 021646b..01005ca 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,7 +20,7 @@ use history::HistoryPlugin;
=use sun::SunPlugin;
=
=const BEGINNING: i32 = 1860;
-const DURATION: i32 = 15;
+const DURATION: i32 = 1;
=
=fn main() {
=    App::new()

WIP: Replay events in explore state

On by Tad Lispy

It kind of works, but is buggy. I think the problem is that events registered at the same time as snapshot are not re-played. Possibly related to the Timestamp value being imprecise (it has 1 second integer precision, where as date is an f32 number of days). It also seems too complicated.

I'm committing nevertheless, as I want to try a different approach and I want to be able to checkout if it doesn't work.

index ab581f3..12ecbea 100644
--- a/src/explore.rs
+++ b/src/explore.rs
@@ -1,5 +1,6 @@
=use crate::history::History;
=use crate::history::TimeTravelRequest;
+use crate::GameState;
=use bevy::prelude::*;
=use bevy_egui::egui;
=use bevy_egui::EguiContexts;
@@ -9,9 +10,7 @@ pub struct ExplorePlugin;
=
=impl Plugin for ExplorePlugin {
=    fn build(&self, app: &mut App) {
-        app.add_systems(
-            Update, paint_ui, /* .run_if(in_state(GameState::Explore)) */
-        );
+        app.add_systems(Update, paint_ui.run_if(in_state(GameState::Explore)));
=    }
=}
=
index af6c46b..fea1d43 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -12,6 +12,7 @@ pub struct HistoryPlugin;
=impl Plugin for HistoryPlugin {
=    fn build(&self, app: &mut App) {
=        app.init_resource::<History>()
+            .init_resource::<Future>()
=            .add_event::<TimeTravelRequest>()
=            .add_systems(
=                PreUpdate,
@@ -23,8 +24,9 @@ impl Plugin for HistoryPlugin {
=            )
=            .add_systems(
=                Update,
-                time_travel, /* .run_if(in_state(GameState::Explore)) */
-            );
+                replay_historical_events.run_if(in_state(GameState::Explore)),
+            )
+            .add_systems(Update, time_travel.run_if(in_state(GameState::Explore)));
=    }
=}
=
@@ -34,6 +36,15 @@ pub struct History {
=    pub snapshots: HashMap<Timestamp, Snapshot>,
=}
=
+/// A collection of events that will happen in the future from the time traveler's perspective
+///
+/// Reset by time_travel. Used by reply_historical_events.
+#[derive(Resource, Default, Clone)]
+pub struct Future {
+    events: HashMap<Timestamp, Vec<HistoricalEvent>>,
+}
+
+#[derive(Clone, Copy)]
=pub enum HistoricalEvent {
=    ConstructionOrder(buildings::ConstructionOrder),
=}
@@ -102,10 +113,47 @@ 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 _ in requests.read() {
+    for request in requests.read() {
=        game_state.set(GameState::Explore);
=
+        future.events.clear();
+        future.events.extend(history.events.clone());
+        future
+            .events
+            .retain(|timestamp, _| timestamp > &Timestamp::from(request.0.date));
+    }
+}
+
+fn replay_historical_events(
+    date: Res<Date>,
+    mut future: ResMut<Future>,
+    mut orders: EventWriter<ConstructionOrder>,
+) {
+    let mut new_future = HashMap::new();
+    // new_future.extend(future.events.clone());
+
+    for timestamp in future.clone().events.into_keys() {
+        if let Some(events) = future.events.get(&timestamp) {
+            if timestamp <= Timestamp::from(date.0) {
+                // Replay the events that are in the past now
+                for event in events {
+                    match event {
+                        HistoricalEvent::ConstructionOrder(order) => orders.send(order.clone()),
+                    };
+                }
+            } else {
+                // Copy the events to the new history
+                new_future.insert(timestamp, events.clone());
+            }
+        }
+    }
+
+    future.events = new_future;
+}
+
=// TODO: Ditch the Timestamp type.
=// If we need keys at all, we can use String from a DayMonth. And maybe keys are not needed at all?
=/// Simplified DayMonth that can be used as a key in a hash map

Scale everything to Blender model of neighborhood

On by Tad Lispy

The model provided by Pedro is 180m×300m, and in Bevy this corresponds to 180.0×300.0 units.

index 1abb8c0..0b176df 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -43,7 +43,7 @@ fn order_construction(
=    let count = buildings.into_iter().count();
=
=    const ROWS: usize = 6;
-    const DISTANCE: usize = 2;
+    const DISTANCE: usize = 300;
=    let x = (count.rem_euclid(ROWS) * DISTANCE) as f32;
=    let z = (count / ROWS * DISTANCE) as f32;
=
@@ -59,14 +59,14 @@ fn spawn_buildings(
=    mut construction_orders: EventReader<ConstructionOrder>,
=) {
=    for ConstructionOrder { x, z } in construction_orders.read() {
-        let model = assets.load("large_buildingB.glb#Scene0");
+        let model = assets.load("neighbourhood_300x180_A.glb#Scene0");
=
=        info!("Spawning a new building at ({x:.2}, {z:.2})!");
=
=        commands
=            .spawn(SceneBundle {
=                scene: model,
-                transform: Transform::from_xyz(*x, 0., *z),
+                transform: Transform::from_xyz(*x, 1., *z),
=                ..default()
=            })
=            .insert(Building);
@@ -86,7 +86,7 @@ fn rollback(
=
=        for transform in snapshot.clone().buildings {
=            // TODO: DRY with spawn_buildings
-            let model = assets.load("large_buildingB.glb#Scene0");
+            let model = assets.load("neighbourhood_300x180_A.glb#Scene0");
=
=            commands
=                .spawn(SceneBundle {
index f2a6489..d8fe453 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -17,7 +17,7 @@ fn setup_ground(
=    mut meshes: ResMut<Assets<Mesh>>,
=    mut materials: ResMut<Assets<StandardMaterial>>,
=) {
-    const SIZE: f32 = 100.;
+    const SIZE: f32 = 1500.;
=    commands
=        .spawn(PbrBundle {
=            mesh: meshes.add(Circle::new(SIZE)),
index 01005ca..ea7c55a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -35,8 +35,8 @@ fn main() {
=        }))
=        .init_state::<GameState>()
=        .add_plugins(camera::CameraPlugin(camera::CameraSettings {
-            elevation: 5.,
-            distance: 20.,
+            elevation: 800.,
+            distance: 1000.,
=            rps: 0.01,
=        }))
=        .add_plugins(GroundPlugin)
index 7d25d8c..6ebbc5f 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -3,7 +3,7 @@ use bevy::prelude::*;
=use std::f32::consts::PI;
=use std::ops::{Mul, Sub};
=
-const ORBIT: f32 = 100.;
+const ORBIT: f32 = 2000.;
=
=pub struct SunPlugin;
=
@@ -20,7 +20,7 @@ struct Sun;
=fn draw_gizmos(mut gizmos: Gizmos, sun: Query<(&Transform, &DirectionalLight), With<Sun>>) {
=    let (position, light) = sun.single();
=
-    gizmos.sphere(position.translation, Quat::default(), 5., light.color);
+    gizmos.sphere(position.translation, Quat::default(), 100., light.color);
=}
=
=fn setup_sunlight(mut commands: Commands) {

Fix some events not being re-played

On by Tad Lispy

Also simplify the logic around it by switching from a HashMap keyed with Timestamp to a vector of EventLogEntries. The date in an entry has the same precision as the date resource.

index fea1d43..d2ddec4 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -32,7 +32,7 @@ impl Plugin for HistoryPlugin {
=
=#[derive(Resource, Default)]
=pub struct History {
-    pub events: HashMap<Timestamp, Vec<HistoricalEvent>>,
+    pub events: Vec<EventLogEntry>,
=    pub snapshots: HashMap<Timestamp, Snapshot>,
=}
=
@@ -41,14 +41,22 @@ pub struct History {
=/// Reset by time_travel. Used by reply_historical_events.
=#[derive(Resource, Default, Clone)]
=pub struct Future {
-    events: HashMap<Timestamp, Vec<HistoricalEvent>>,
+    events: Vec<EventLogEntry>,
=}
=
+/// A wrapper for any kind of event that should be replayed in explore mode
=#[derive(Clone, Copy)]
=pub enum HistoricalEvent {
=    ConstructionOrder(buildings::ConstructionOrder),
=}
=
+/// A historical event together with it's date
+#[derive(Clone, Copy)]
+pub struct EventLogEntry {
+    date: DayMonth,
+    event: HistoricalEvent,
+}
+
=impl Display for HistoricalEvent {
=    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
=        match self {
@@ -67,13 +75,12 @@ fn register_historical_events(
=    date: Res<Date>,
=    mut construction_orders: EventReader<ConstructionOrder>,
=) {
-    let key: Timestamp = date.0.into();
-    let historical_events = history.events.entry(key).or_insert(Vec::new());
-
+    let date = date.0;
=    for event in construction_orders.read() {
-        let historical_event = HistoricalEvent::ConstructionOrder(event.to_owned());
-        info!("Registering a historical event: {historical_event}");
-        historical_events.push(historical_event);
+        let event = HistoricalEvent::ConstructionOrder(event.to_owned());
+        info!("Registering a historical event: {event}");
+
+        history.events.push(EventLogEntry { event, date });
=    }
=}
=
@@ -119,11 +126,12 @@ fn time_travel(
=    for request in requests.read() {
=        game_state.set(GameState::Explore);
=
-        future.events.clear();
-        future.events.extend(history.events.clone());
-        future
+        future.events = history
=            .events
-            .retain(|timestamp, _| timestamp > &Timestamp::from(request.0.date));
+            .clone()
+            .into_iter()
+            .filter(|event| event.date >= request.0.date)
+            .collect();
=    }
=}
=
@@ -132,26 +140,16 @@ fn replay_historical_events(
=    mut future: ResMut<Future>,
=    mut orders: EventWriter<ConstructionOrder>,
=) {
-    let mut new_future = HashMap::new();
-    // new_future.extend(future.events.clone());
-
-    for timestamp in future.clone().events.into_keys() {
-        if let Some(events) = future.events.get(&timestamp) {
-            if timestamp <= Timestamp::from(date.0) {
-                // Replay the events that are in the past now
-                for event in events {
-                    match event {
-                        HistoricalEvent::ConstructionOrder(order) => orders.send(order.clone()),
-                    };
-                }
-            } else {
-                // Copy the events to the new history
-                new_future.insert(timestamp, events.clone());
-            }
+    future.events.retain(|entry| {
+        if entry.date <= date.0 {
+            match entry.event {
+                HistoricalEvent::ConstructionOrder(order) => orders.send(order.clone()),
+            };
+            false
+        } else {
+            true
=        }
-    }
-
-    future.events = new_future;
+    });
=}
=
=// TODO: Ditch the Timestamp type.

Update dependencies

On by Tad Lispy

index 4c1f6cb..64c3233 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -80,9 +80,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
=
=[[package]]
=name = "ahash"
-version = "0.8.10"
+version = "0.8.11"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
=dependencies = [
= "cfg-if",
= "getrandom",
@@ -181,9 +181,9 @@ dependencies = [
=
=[[package]]
=name = "arboard"
-version = "3.3.1"
+version = "3.3.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1faa3c733d9a3dd6fbaf85da5d162a2e03b2e0033a90dceb0e2a90fdd1e5380a"
+checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58"
=dependencies = [
= "clipboard-win",
= "core-graphics",
@@ -1260,10 +1260,11 @@ dependencies = [
=
=[[package]]
=name = "cc"
-version = "1.0.88"
+version = "1.0.89"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc"
+checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
=dependencies = [
+ "jobserver",
= "libc",
=]
=
@@ -1302,7 +1303,7 @@ checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
=dependencies = [
= "glob",
= "libc",
- "libloading 0.8.1",
+ "libloading 0.8.2",
=]
=
=[[package]]
@@ -1551,7 +1552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
=dependencies = [
= "bitflags 2.4.2",
- "libloading 0.8.1",
+ "libloading 0.8.2",
= "winapi",
=]
=
@@ -1592,7 +1593,7 @@ version = "0.5.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
=dependencies = [
- "libloading 0.8.1",
+ "libloading 0.8.2",
=]
=
=[[package]]
@@ -2109,7 +2110,7 @@ dependencies = [
= "bitflags 2.4.2",
= "com",
= "libc",
- "libloading 0.8.1",
+ "libloading 0.8.2",
= "thiserror",
= "widestring",
= "winapi",
@@ -2286,6 +2287,15 @@ version = "0.3.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
=
+[[package]]
+name = "jobserver"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
+dependencies = [
+ "libc",
+]
+
=[[package]]
=name = "jpeg-decoder"
=version = "0.3.1"
@@ -2294,9 +2304,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
=
=[[package]]
=name = "js-sys"
-version = "0.3.68"
+version = "0.3.69"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
=dependencies = [
= "wasm-bindgen",
=]
@@ -2308,7 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
=checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
=dependencies = [
= "libc",
- "libloading 0.8.1",
+ "libloading 0.8.2",
= "pkg-config",
=]
=
@@ -2368,12 +2378,12 @@ dependencies = [
=
=[[package]]
=name = "libloading"
-version = "0.8.1"
+version = "0.8.2"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
+checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164"
=dependencies = [
= "cfg-if",
- "windows-sys 0.48.0",
+ "windows-targets 0.52.4",
=]
=
=[[package]]
@@ -3133,7 +3143,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
=dependencies = [
= "aho-corasick",
= "memchr",
- "regex-automata 0.4.5",
+ "regex-automata 0.4.6",
= "regex-syntax 0.8.2",
=]
=
@@ -3148,9 +3158,9 @@ dependencies = [
=
=[[package]]
=name = "regex-automata"
-version = "0.4.5"
+version = "0.4.6"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
=dependencies = [
= "aho-corasick",
= "memchr",
@@ -3171,9 +3181,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
=
=[[package]]
=name = "renderdoc-sys"
-version = "1.0.0"
+version = "1.1.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
=
=[[package]]
=name = "rodio"
@@ -3446,9 +3456,9 @@ dependencies = [
=
=[[package]]
=name = "sysinfo"
-version = "0.30.5"
+version = "0.30.6"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2"
+checksum = "6746919caf9f2a85bff759535664c060109f21975c5ac2e8652e60102bd4d196"
=dependencies = [
= "cfg-if",
= "core-foundation-sys",
@@ -3767,9 +3777,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
=
=[[package]]
=name = "walkdir"
-version = "2.4.0"
+version = "2.5.0"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
=dependencies = [
= "same-file",
= "winapi-util",
@@ -3783,9 +3793,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
=
=[[package]]
=name = "wasm-bindgen"
-version = "0.2.91"
+version = "0.2.92"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
=dependencies = [
= "cfg-if",
= "wasm-bindgen-macro",
@@ -3793,9 +3803,9 @@ dependencies = [
=
=[[package]]
=name = "wasm-bindgen-backend"
-version = "0.2.91"
+version = "0.2.92"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
=dependencies = [
= "bumpalo",
= "log",
@@ -3808,9 +3818,9 @@ dependencies = [
=
=[[package]]
=name = "wasm-bindgen-futures"
-version = "0.4.41"
+version = "0.4.42"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
=dependencies = [
= "cfg-if",
= "js-sys",
@@ -3820,9 +3830,9 @@ dependencies = [
=
=[[package]]
=name = "wasm-bindgen-macro"
-version = "0.2.91"
+version = "0.2.92"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
=dependencies = [
= "quote",
= "wasm-bindgen-macro-support",
@@ -3830,9 +3840,9 @@ dependencies = [
=
=[[package]]
=name = "wasm-bindgen-macro-support"
-version = "0.2.91"
+version = "0.2.92"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
=dependencies = [
= "proc-macro2",
= "quote",
@@ -3843,9 +3853,9 @@ dependencies = [
=
=[[package]]
=name = "wasm-bindgen-shared"
-version = "0.2.91"
+version = "0.2.92"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
=
=[[package]]
=name = "wayland-backend"
@@ -4000,9 +4010,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
=
=[[package]]
=name = "wgpu"
-version = "0.19.2"
+version = "0.19.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9274d073bfb0cad6c53c575d3f4815617c9ccc134cba3ead1fa5fa51e71595c"
+checksum = "a4b1213b52478a7631d6e387543ed8f642bc02c578ef4e3b49aca2a29a7df0cb"
=dependencies = [
= "arrayvec",
= "cfg-if",
@@ -4025,9 +4035,9 @@ dependencies = [
=
=[[package]]
=name = "wgpu-core"
-version = "0.19.2"
+version = "0.19.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c8bb6a5c62ad78bd683609d525cde6efb6e7cf39e008ea0b8e42518ce18ea81"
+checksum = "f9f6b033c2f00ae0bc8ea872c5989777c60bc241aac4e58b24774faa8b391f78"
=dependencies = [
= "arrayvec",
= "bit-vec",
@@ -4051,9 +4061,9 @@ dependencies = [
=
=[[package]]
=name = "wgpu-hal"
-version = "0.19.2"
+version = "0.19.3"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7b9a56d44851cc0f51bf2f8b5ed8af5cf6e4bdb178fd5786ea2b4771e2edf0"
+checksum = "49f972c280505ab52ffe17e94a7413d9d54b58af0114ab226b9fc4999a47082e"
=dependencies = [
= "android_system_properties",
= "arrayvec",
@@ -4073,10 +4083,11 @@ dependencies = [
= "js-sys",
= "khronos-egl",
= "libc",
- "libloading 0.8.1",
+ "libloading 0.8.2",
= "log",
= "metal",
= "naga",
+ "ndk-sys 0.5.0+25.2.9519653",
= "objc",
= "once_cell",
= "parking_lot",
@@ -4402,9 +4413,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
=
=[[package]]
=name = "winit"
-version = "0.29.11"
+version = "0.29.13"
=source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "272be407f804517512fdf408f0fe6c067bf24659a913c61af97af176bfd5aa92"
+checksum = "2b9d7047a2a569d5a81e3be098dcd8153759909b127477f4397e03cf1006d90a"
=dependencies = [
= "ahash",
= "android-activity",
@@ -4477,7 +4488,7 @@ dependencies = [
= "as-raw-xcb-connection",
= "gethostname",
= "libc",
- "libloading 0.8.1",
+ "libloading 0.8.2",
= "once_cell",
= "rustix",
= "x11rb-protocol",
index e4f7bad..4c2cd61 100644
--- a/flake.lock
+++ b/flake.lock
@@ -38,11 +38,11 @@
=    },
=    "nixpkgs": {
=      "locked": {
-        "lastModified": 1709150264,
-        "narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
+        "lastModified": 1709237383,
+        "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
=        "owner": "NixOS",
=        "repo": "nixpkgs",
-        "rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
+        "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
=        "type": "github"
=      },
=      "original": {
@@ -81,11 +81,11 @@
=        "nixpkgs": "nixpkgs_2"
=      },
=      "locked": {
-        "lastModified": 1709172595,
-        "narHash": "sha256-0oYeE5VkhnPA7YBl+0Utq2cYoHcfsEhSGwraCa27Vs8=",
+        "lastModified": 1709519692,
+        "narHash": "sha256-+ICGcASuUGpx82io6FVkMW7Pv4dvEl1v9A0ZtBKT41A=",
=        "owner": "oxalica",
=        "repo": "rust-overlay",
-        "rev": "72fa0217f76020ad3aeb2dd9dd72490905b23b6f",
+        "rev": "30c3af18405567115958c577c62548bdc5a251e7",
=        "type": "github"
=      },
=      "original": {

Implement the export function that prints history

On by Tad Lispy

For now just using Debug trait on History type. To be improved.

index 0b176df..aed2e9a 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -20,7 +20,7 @@ impl Plugin for BuildingsPlugin {
=/// 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)]
+#[derive(Event, Clone, Copy, Debug)]
=pub struct ConstructionOrder {
=    pub x: f32,
=    pub z: f32,
index 12ecbea..c60205e 100644
--- a/src/explore.rs
+++ b/src/explore.rs
@@ -22,6 +22,11 @@ fn paint_ui(
=    egui::SidePanel::right("explore_ui").show(contexts.ctx_mut(), |ui| {
=        ui.heading("Explore");
=
+        let export_button = ui.button("Export");
+        if export_button.clicked() {
+            info!("The history:\n\n{history:?}")
+        }
+
=        egui::ScrollArea::vertical().show(ui, |ui| {
=            for (timestamp, snapshot) in history
=                .snapshots
index d2ddec4..86df872 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -30,7 +30,7 @@ impl Plugin for HistoryPlugin {
=    }
=}
=
-#[derive(Resource, Default)]
+#[derive(Resource, Default, Debug)]
=pub struct History {
=    pub events: Vec<EventLogEntry>,
=    pub snapshots: HashMap<Timestamp, Snapshot>,
@@ -45,13 +45,13 @@ pub struct Future {
=}
=
=/// A wrapper for any kind of event that should be replayed in explore mode
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
=pub enum HistoricalEvent {
=    ConstructionOrder(buildings::ConstructionOrder),
=}
=
=/// A historical event together with it's date
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
=pub struct EventLogEntry {
=    date: DayMonth,
=    event: HistoricalEvent,
@@ -86,7 +86,7 @@ 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
-#[derive(Clone)]
+#[derive(Clone, Debug)]
=pub struct Snapshot {
=    pub date: DayMonth,
=    pub buildings: Vec<Transform>,
@@ -155,7 +155,7 @@ fn replay_historical_events(
=// TODO: Ditch the Timestamp type.
=// If we need keys at all, we can use String from a DayMonth. And maybe keys are not needed at all?
=/// Simplified DayMonth that can be used as a key in a hash map
-#[derive(PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)]
+#[derive(PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy, Debug)]
=pub struct Timestamp {
=    pub year: i32,
=    pub month: Month,

Increase the land size. Control it via a resource

On by Tad Lispy

There is a new resource of type Settings. Currently there are two fields: land radius and sun gap, meaning how far the sun is outside of the land circle (extra distance from the origin point).

index d8fe453..aad64a3 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -1,3 +1,4 @@
+use crate::Settings;
=use bevy::prelude::*;
=use std::f32::consts::FRAC_PI_2;
=
@@ -16,11 +17,11 @@ fn setup_ground(
=    mut commands: Commands,
=    mut meshes: ResMut<Assets<Mesh>>,
=    mut materials: ResMut<Assets<StandardMaterial>>,
+    settings: Res<Settings>,
=) {
-    const SIZE: f32 = 1500.;
=    commands
=        .spawn(PbrBundle {
-            mesh: meshes.add(Circle::new(SIZE)),
+            mesh: meshes.add(Circle::new(settings.land_radius)),
=            material: materials.add(StandardMaterial {
=                base_color: Color::hsl(150.0, 0.3, 0.3),
=                perceptual_roughness: 0.8,
index ea7c55a..f3ad594 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -33,10 +33,14 @@ fn main() {
=            }),
=            ..default()
=        }))
+        .insert_resource(Settings {
+            land_radius: 2000.,
+            sun_gap: 100.,
+        })
=        .init_state::<GameState>()
=        .add_plugins(camera::CameraPlugin(camera::CameraSettings {
-            elevation: 800.,
-            distance: 1000.,
+            elevation: 1200.,
+            distance: 1500.,
=            rps: 0.01,
=        }))
=        .add_plugins(GroundPlugin)
@@ -52,6 +56,12 @@ fn main() {
=        .run()
=}
=
+#[derive(Debug, Resource)]
+pub struct Settings {
+    land_radius: f32,
+    sun_gap: f32,
+}
+
=fn greet() {
=    info!("Let's build the city on rock and roll!")
=}
index 6ebbc5f..02a2168 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -1,10 +1,9 @@
=use crate::date::Date;
+use crate::Settings;
=use bevy::prelude::*;
=use std::f32::consts::PI;
=use std::ops::{Mul, Sub};
=
-const ORBIT: f32 = 2000.;
-
=pub struct SunPlugin;
=
=impl Plugin for SunPlugin {
@@ -23,10 +22,12 @@ fn draw_gizmos(mut gizmos: Gizmos, sun: Query<(&Transform, &DirectionalLight), W
=    gizmos.sphere(position.translation, Quat::default(), 100., light.color);
=}
=
-fn setup_sunlight(mut commands: Commands) {
+fn setup_sunlight(mut commands: Commands, settings: Res<Settings>) {
+    let orbit = settings.land_radius + settings.sun_gap;
+
=    commands
=        .spawn(DirectionalLightBundle {
-            transform: Transform::from_translation(Vec3::NEG_Y * ORBIT)
+            transform: Transform::from_translation(Vec3::NEG_Y * orbit)
=                .looking_at(Vec3::ZERO, Vec3::Y),
=            directional_light: DirectionalLight {
=                shadows_enabled: true,
@@ -38,11 +39,13 @@ fn setup_sunlight(mut commands: Commands) {
=        .insert(Sun);
=}
=
-fn move_sun(mut sun: Query<&mut Transform, With<Sun>>, date: Res<Date>) {
+fn move_sun(mut sun: Query<&mut Transform, With<Sun>>, date: Res<Date>, settings: Res<Settings>) {
+    let orbit = settings.land_radius + settings.sun_gap;
+
=    let daytime = date.0.time_of_day();
=    let angle = daytime * 2.0 * PI - PI;
=    let rotation = Quat::from_rotation_x(angle) * Quat::from_rotation_z(1.0);
-    let translation = rotation.mul_vec3(Vec3::Y) * ORBIT;
+    let translation = rotation.mul_vec3(Vec3::Y) * orbit;
=
=    let mut transform = sun.single_mut();
=    transform.translation = translation;

Separate export logic to own module

On by Tad Lispy

index c60205e..d9b8ac5 100644
--- a/src/explore.rs
+++ b/src/explore.rs
@@ -1,5 +1,6 @@
=use crate::history::History;
=use crate::history::TimeTravelRequest;
+use crate::pgsql_export;
=use crate::GameState;
=use bevy::prelude::*;
=use bevy_egui::egui;
@@ -24,7 +25,9 @@ fn paint_ui(
=
=        let export_button = ui.button("Export");
=        if export_button.clicked() {
-            info!("The history:\n\n{history:?}")
+            let exported = pgsql_export::export(history.as_ref());
+            // TODO: Save a file
+            info!("Export:\n\n{exported}");
=        }
=
=        egui::ScrollArea::vertical().show(ui, |ui| {
index f3ad594..342b263 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ mod day_month;
=mod explore;
=mod ground;
=mod history;
+mod pgsql_export;
=mod sun;
=
=use bevy::prelude::*;
new file mode 100644
index 0000000..de5cb8b
--- /dev/null
+++ b/src/pgsql_export.rs
@@ -0,0 +1,5 @@
+use crate::history::History;
+
+pub fn export(history: &History) -> String {
+    format!("The history:\n\n{history:?}")
+}

Apply custom formatting for export string

On by Tad Lispy

It's not SQL yet, but demonstrates the ability to format history differently.

index 86df872..12a9a09 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -53,8 +53,8 @@ pub enum HistoricalEvent {
=/// A historical event together with it's date
=#[derive(Clone, Copy, Debug)]
=pub struct EventLogEntry {
-    date: DayMonth,
-    event: HistoricalEvent,
+    pub date: DayMonth,
+    pub event: HistoricalEvent,
=}
=
=impl Display for HistoricalEvent {
index de5cb8b..ee7e72d 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,5 +1,24 @@
-use crate::history::History;
+use std::fmt::Display;
+
+use crate::{buildings::ConstructionOrder, history::History};
=
=pub fn export(history: &History) -> String {
-    format!("The history:\n\n{history:?}")
+    history.to_string()
+}
+
+impl Display for History {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        writeln!(f, "The History of Otterhide")?;
+        for event in self.events.iter() {
+            let date = event.date;
+            let description = match event.event {
+                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder { x, z }) => {
+                    format!("A new building is constructed at ({x}, {z}).")
+                }
+            };
+            writeln!(f, "  - {date}: {description}")?;
+        }
+
+        writeln!(f, "This is The End")
+    }
=}

Fix neighborhood meshes flickering

On by Tad Lispy

They are now elevated 4m above the ground.

index aed2e9a..72d6b2d 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -66,7 +66,7 @@ fn spawn_buildings(
=        commands
=            .spawn(SceneBundle {
=                scene: model,
-                transform: Transform::from_xyz(*x, 1., *z),
+                transform: Transform::from_xyz(*x, 4., *z),
=                ..default()
=            })
=            .insert(Building);

Added road around and increased base thickness

On by pedrolinux

index 0081542..95258ed 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ
index 32a8739..59dc6d5 100644
Binary files a/assets/neighbourhood_300x180_A.glb and b/assets/neighbourhood_300x180_A.glb differ

Write actual SQL string when exporting

On by Tad Lispy

I didn't check if it's valid, but that's not the point at this moment :D

index ee7e72d..8825ee4 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -8,17 +8,35 @@ pub fn export(history: &History) -> String {
=
=impl Display for History {
=    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        writeln!(f, "The History of Otterhide")?;
+        writeln!(f, "Create table neighborhood (")?;
+        writeln!(f, "   id primary key int autoincrement,")?;
+        writeln!(f, "   date timestamp without time zone not null,")?;
+        writeln!(f, "   coordinates point not null")?;
+        writeln!(f, ");")?;
+
=        for event in self.events.iter() {
=            let date = event.date;
+            let year = date.year();
+            let month = date.month() as i32;
+            let day = 01;
+            let hour = date.hour();
+            let minute = date.minute() as i32;
+            let second = 00;
+
=            let description = match event.event {
=                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder { x, z }) => {
-                    format!("A new building is constructed at ({x}, {z}).")
+                    writeln!(f, "Insert into neighborhood (")?;
+                    writeln!(f, "   date,")?;
+                    writeln!(f, "   coordinates")?;
+                    writeln!(f, ") values (")?;
+                    writeln!(
+                        f,
+                        "   {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
+                    )?;
+                    writeln!(f, "   ({x:.3}, {z:.3})")?;
+                    writeln!(f, ");")?;
=                }
=            };
-            writeln!(f, "  - {date}: {description}")?;
=        }
-
-        writeln!(f, "This is The End")
=    }
=}

Fix SQL export: always return a value

On by Tad Lispy

index 8825ee4..eb68657 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -38,5 +38,7 @@ impl Display for History {
=                }
=            };
=        }
+
+        writeln!(f, "-- This is the end")
=    }
=}

Added roads and initial house spots. Updated outer road side

On by pedrolinux

index 95258ed..af9d75a 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ
index 59dc6d5..f7becbf 100644
Binary files a/assets/neighbourhood_300x180_A.glb and b/assets/neighbourhood_300x180_A.glb differ

Scale the building model

On by Tad Lispy

It was tiny.

index 7284b65..fd46796 100644
Binary files a/art/large_buildingB.blend and b/art/large_buildingB.blend differ
index 0e55573..b1766af 100644
Binary files a/assets/large_buildingB.glb and b/assets/large_buildingB.glb differ

Separate buildings and districts logic

On by Tad Lispy

There are now two entities for which we track history and export data.

index 72d6b2d..d962873 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -30,6 +30,8 @@ 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 new_month_events: EventReader<NewMonth>,
@@ -43,7 +45,7 @@ fn order_construction(
=    let count = buildings.into_iter().count();
=
=    const ROWS: usize = 6;
-    const DISTANCE: usize = 300;
+    const DISTANCE: usize = 10;
=    let x = (count.rem_euclid(ROWS) * DISTANCE) as f32;
=    let z = (count / ROWS * DISTANCE) as f32;
=
@@ -59,7 +61,7 @@ fn spawn_buildings(
=    mut construction_orders: EventReader<ConstructionOrder>,
=) {
=    for ConstructionOrder { x, z } in construction_orders.read() {
-        let model = assets.load("neighbourhood_300x180_A.glb#Scene0");
+        let model = assets.load(MODEL_PATH);
=
=        info!("Spawning a new building at ({x:.2}, {z:.2})!");
=
@@ -86,7 +88,7 @@ fn rollback(
=
=        for transform in snapshot.clone().buildings {
=            // TODO: DRY with spawn_buildings
-            let model = assets.load("neighbourhood_300x180_A.glb#Scene0");
+            let model = assets.load(MODEL_PATH);
=
=            commands
=                .spawn(SceneBundle {
new file mode 100644
index 0000000..9ec6eb6
--- /dev/null
+++ b/src/districts.rs
@@ -0,0 +1,100 @@
+use crate::date::NewMonth;
+use crate::history::TimeTravelRequest;
+use crate::GameState;
+use bevy::prelude::*;
+
+pub struct DistrictsPlugin;
+
+impl Plugin for DistrictsPlugin {
+    fn build(&self, app: &mut App) {
+        app.add_event::<NewDistrictEstablished>()
+            .add_systems(
+                Update,
+                establish_new_districts.run_if(in_state(GameState::Simulate)),
+            )
+            .add_systems(Update, spawn_districts)
+            .add_systems(Update, rollback);
+    }
+}
+
+/// 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 NewDistrictEstablished {
+    pub x: f32,
+    pub z: f32,
+}
+
+/// A tag for district entities
+#[derive(Component)]
+pub struct District;
+
+const MODEL_PATH: &str = "neighbourhood_300x180_A.glb#Scene0";
+
+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 = 6;
+    const DISTANCE: usize = 300;
+    let x = (count.rem_euclid(ROWS) * DISTANCE) as f32;
+    let z = (count / ROWS * DISTANCE) as f32;
+
+    info!("New district established at ({x:.2}, {z:.2})!");
+
+    new_districts.send(NewDistrictEstablished { x, z });
+}
+
+fn spawn_districts(
+    mut commands: Commands,
+    assets: ResMut<AssetServer>,
+    mut new_districts: EventReader<NewDistrictEstablished>,
+) {
+    for NewDistrictEstablished { x, z } in new_districts.read() {
+        let model = assets.load(MODEL_PATH);
+
+        info!("Spawning a new district at ({x:.2}, {z:.2})!");
+
+        commands
+            .spawn(SceneBundle {
+                scene: model,
+                transform: Transform::from_xyz(*x, 4., *z),
+                ..default()
+            })
+            .insert(District);
+    }
+}
+
+fn rollback(
+    mut requests: EventReader<TimeTravelRequest>,
+    districts: Query<Entity, With<District>>,
+    assets: ResMut<AssetServer>,
+    mut commands: Commands,
+) {
+    for TimeTravelRequest(snapshot) in requests.read() {
+        for entity in districts.iter() {
+            commands.entity(entity).despawn_recursive();
+        }
+
+        for transform in snapshot.clone().districts {
+            // TODO: DRY with spawn_buildings
+            let model = assets.load(MODEL_PATH);
+
+            commands
+                .spawn(SceneBundle {
+                    scene: model,
+                    transform: transform.clone(),
+                    ..default()
+                })
+                .insert(District);
+        }
+    }
+}
index 12a9a09..272697d 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -2,7 +2,8 @@ use crate::buildings::ConstructionOrder;
=use crate::buildings::{self, Building};
=use crate::date::{Date, NewMonth};
=use crate::day_month::{DayMonth, Month};
-use crate::GameState;
+use crate::districts::{District, NewDistrictEstablished};
+use crate::{districts, GameState};
=use bevy::prelude::*;
=use bevy::utils::HashMap;
=use std::fmt::Display;
@@ -48,6 +49,7 @@ pub struct Future {
=#[derive(Clone, Copy, Debug)]
=pub enum HistoricalEvent {
=    ConstructionOrder(buildings::ConstructionOrder),
+    NewDistrictEstablished(districts::NewDistrictEstablished),
=}
=
=/// A historical event together with it's date
@@ -66,6 +68,9 @@ impl Display for HistoricalEvent {
=                    "construction of a new building ordered at ({x:.2}, {z:.2})",
=                )
=            }
+            HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished { x, z }) => {
+                write!(f, "New district established at ({x:.2}, {z:.2})",)
+            }
=        }
=    }
=}
@@ -74,8 +79,15 @@ fn register_historical_events(
=    mut history: ResMut<History>,
=    date: Res<Date>,
=    mut construction_orders: EventReader<ConstructionOrder>,
+    mut new_districts: EventReader<NewDistrictEstablished>,
=) {
=    let date = date.0;
+    for event in new_districts.read() {
+        let event = HistoricalEvent::NewDistrictEstablished(event.to_owned());
+        info!("Registering a historical event: {event}");
+
+        history.events.push(EventLogEntry { event, date });
+    }
=    for event in construction_orders.read() {
=        let event = HistoricalEvent::ConstructionOrder(event.to_owned());
=        info!("Registering a historical event: {event}");
@@ -90,6 +102,7 @@ fn register_historical_events(
=pub struct Snapshot {
=    pub date: DayMonth,
=    pub buildings: Vec<Transform>,
+    pub districts: Vec<Transform>,
=}
=
=fn take_snapshot(
@@ -97,6 +110,7 @@ fn take_snapshot(
=    date: Res<Date>,
=    mut history: ResMut<History>,
=    buildings: Query<&Transform, With<Building>>,
+    districts: Query<&Transform, With<District>>,
=) {
=    if new_month.read().count() == 0 {
=        return;
@@ -104,9 +118,11 @@ fn take_snapshot(
=
=    let timestamp: Timestamp = date.0.into();
=    let buildings = buildings.iter().map(|transform| *transform).collect();
+    let districts = districts.iter().map(|transform| *transform).collect();
=    let snapshot = Snapshot {
=        date: date.0,
=        buildings,
+        districts,
=    };
=    history.snapshots.insert(timestamp.clone(), snapshot);
=
@@ -139,11 +155,17 @@ fn replay_historical_events(
=    date: Res<Date>,
=    mut future: ResMut<Future>,
=    mut orders: EventWriter<ConstructionOrder>,
+    mut districts: EventWriter<NewDistrictEstablished>,
=) {
=    future.events.retain(|entry| {
=        if entry.date <= date.0 {
=            match entry.event {
-                HistoricalEvent::ConstructionOrder(order) => orders.send(order.clone()),
+                HistoricalEvent::ConstructionOrder(order) => {
+                    orders.send(order.clone());
+                }
+                HistoricalEvent::NewDistrictEstablished(district) => {
+                    districts.send(district.clone());
+                }
=            };
=            false
=        } else {
index 342b263..4040640 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@ mod buildings;
=mod camera;
=mod date;
=mod day_month;
+mod districts;
=mod explore;
=mod ground;
=mod history;
@@ -15,6 +16,7 @@ use date::Date;
=use date::DatePlugin;
=use date::NewMonth;
=use day_month::DayMonth;
+use districts::DistrictsPlugin;
=use explore::ExplorePlugin;
=use ground::GroundPlugin;
=use history::HistoryPlugin;
@@ -45,6 +47,7 @@ fn main() {
=            rps: 0.01,
=        }))
=        .add_plugins(GroundPlugin)
+        .add_plugins(DistrictsPlugin)
=        .add_plugins(BuildingsPlugin)
=        .add_plugins(DatePlugin)
=        .add_plugins(SunPlugin)
index eb68657..431e980 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -1,6 +1,8 @@
=use std::fmt::Display;
=
-use crate::{buildings::ConstructionOrder, history::History};
+use crate::buildings::ConstructionOrder;
+use crate::districts::NewDistrictEstablished;
+use crate::history::History;
=
=pub fn export(history: &History) -> String {
=    history.to_string()
@@ -8,7 +10,12 @@ pub fn export(history: &History) -> String {
=
=impl Display for History {
=    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        writeln!(f, "Create table neighborhood (")?;
+        writeln!(f, "Create table districts (")?;
+        writeln!(f, "   id primary key int autoincrement,")?;
+        writeln!(f, "   date timestamp without time zone not null,")?;
+        writeln!(f, "   coordinates point not null")?;
+        writeln!(f, ");")?;
+        writeln!(f, "Create table buildings (")?;
=        writeln!(f, "   id primary key int autoincrement,")?;
=        writeln!(f, "   date timestamp without time zone not null,")?;
=        writeln!(f, "   coordinates point not null")?;
@@ -23,9 +30,23 @@ impl Display for History {
=            let minute = date.minute() as i32;
=            let second = 00;
=
-            let description = match event.event {
+            match event.event {
=                crate::history::HistoricalEvent::ConstructionOrder(ConstructionOrder { x, z }) => {
-                    writeln!(f, "Insert into neighborhood (")?;
+                    writeln!(f, "Insert into buildings (")?;
+                    writeln!(f, "   date,")?;
+                    writeln!(f, "   coordinates")?;
+                    writeln!(f, ") values (")?;
+                    writeln!(
+                        f,
+                        "   {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
+                    )?;
+                    writeln!(f, "   ({x:.3}, {z:.3})")?;
+                    writeln!(f, ");")?;
+                }
+                crate::history::HistoricalEvent::NewDistrictEstablished(
+                    NewDistrictEstablished { x, z },
+                ) => {
+                    writeln!(f, "Insert into districts (")?;
=                    writeln!(f, "   date,")?;
=                    writeln!(f, "   coordinates")?;
=                    writeln!(f, ") values (")?;

Added 16 scenes with road blocks with naming 'RoadWSEN' (cardinal initials, West, South, East, North)

On by pedrolinux

index af9d75a..1ca9ff5 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ
index f7becbf..5ad31b6 100644
Binary files a/assets/neighbourhood_300x180_A.glb and b/assets/neighbourhood_300x180_A.glb differ

Filled center of road blocks of curves and intersections with asphalt color

On by pedrolinux

index 1ca9ff5..7cf7372 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ

Changed scene name to 000 to appear in first place

On by pedrolinux

index 7cf7372..9627e9b 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ
index 5ad31b6..7948517 100644
Binary files a/assets/neighbourhood_300x180_A.glb and b/assets/neighbourhood_300x180_A.glb differ

Write some logic and tests regarding roads

On by Tad Lispy

Honestly I'm not sure how much of it will be useful. I'm mostly committing so I can delete this code without a feeling of loss :P

index 4040640..9561877 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,7 @@ mod explore;
=mod ground;
=mod history;
=mod pgsql_export;
+mod roads;
=mod sun;
=
=use bevy::prelude::*;
new file mode 100644
index 0000000..2d243e7
--- /dev/null
+++ b/src/roads.rs
@@ -0,0 +1,331 @@
+use core::fmt;
+use std::fmt::Display;
+use std::ops::Not;
+
+#[derive(Debug, Default, PartialEq)]
+struct Road {
+    directions: Directions,
+    sides: Sides,
+}
+
+impl Road {
+    pub fn can_combine(&self, other: &Self) -> bool {
+        self.sides.overlaps(&other.sides).not()
+    }
+
+    pub fn combine(mut 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: 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);
+        self
+    }
+
+    pub fn mark_neighbouring_district(mut self, quadrant: &Quadrant) -> Self {
+        let side = Sides::from(quadrant.clone());
+        self.sides = self.sides.combine(&side);
+        self
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Direction {
+    North = 0b0001,
+    East = 0b0010,
+    South = 0b0100,
+    West = 0b1000,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Quadrant {
+    NorthEast = 0b0001,
+    EastSouth = 0b0010,
+    SouthWest = 0b0100,
+    WestNorth = 0b1000,
+}
+
+#[derive(PartialEq, Eq)]
+struct Directions(u8);
+
+impl From<Direction> for Directions {
+    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 {
+    fn from(value: u8) -> Self {
+        Self(value & 0b1111)
+    }
+}
+
+impl From<Directions> for u8 {
+    fn from(value: Directions) -> Self {
+        value.0
+    }
+}
+
+impl fmt::Debug for Directions {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "Sides({})", self.to_string())
+    }
+}
+
+impl Display for Directions {
+    /// Use Unicode Box Drawing to show where roads are
+    ///
+    /// See https://en.wikipedia.org/wiki/Box_Drawing
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let character = match self.0 {
+            // Empty
+            0b0000 => ' ',
+
+            // One direction (dead end)
+            0b0001 => '╹',
+            0b0010 => '╺',
+            0b0100 => '╻',
+            0b1000 => '╸',
+
+            // Straight road
+            0b0101 => '┃',
+            0b1010 => '━',
+
+            // Corner
+            0b0011 => '┗',
+            0b0110 => '┏',
+            0b1001 => '┛',
+            0b1100 => '┓',
+
+            // T junction
+            0b0111 => '┣',
+            0b1011 => '┻',
+            0b1101 => '┫',
+            0b1110 => '┳',
+
+            // Cross roads
+            0b1111 => '╋',
+
+            // No other values are possible
+            _ => panic!(
+                "Incorrect bit field passed as a road direction: {value:b}",
+                value = self.0
+            ),
+        };
+        write!(f, "{character}")
+    }
+}
+
+// struct Sides(u8);
+
+#[cfg(test)]
+mod roads_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(), "┫");
+    }
+
+    #[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
+        );
+    }
+}
+
+// SIDES
+
+#[derive(PartialEq, Eq)]
+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);
+    }
+}

Separate road tiles to own .blend and .glb files

On by Tad Lispy

We need the scenes in the file to be ordered according to the bitfield value of a RoadSection type. The easiest was to move the tiles to own file.

new file mode 100644
index 0000000..4937b95
Binary files /dev/null and b/art/roads.blend differ
new file mode 100644
index 0000000..ecc8e3f
Binary files /dev/null and b/assets/roads.glb differ

Implement roads combination system

On by Tad Lispy

The code is not cleaned up (I'm too tired for that now) and there is a lot of dead code that most likely needs to be deleted.

To demonstrate the roads features I disabled districts and buildings systems.

To get a single scene from a .glb asset, it needs to be loaded. For this reason there is now a GameState::Loading variant, that does what it's name suggests.

index d962873..a2369f5 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -12,7 +12,7 @@ impl Plugin for BuildingsPlugin {
=                Update,
=                order_construction.run_if(in_state(GameState::Simulate)),
=            )
-            .add_systems(Update, spawn_buildings)
+            // .add_systems(Update, spawn_buildings)
=            .add_systems(Update, rollback);
=    }
=}
index 9ec6eb6..a39a829 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -8,10 +8,11 @@ pub struct DistrictsPlugin;
=impl Plugin for DistrictsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<NewDistrictEstablished>()
-            .add_systems(
-                Update,
-                establish_new_districts.run_if(in_state(GameState::Simulate)),
-            )
+            // Temporarily disabled to make the roads visible
+            // .add_systems(
+            //     Update,
+            //     establish_new_districts.run_if(in_state(GameState::Simulate)),
+            // )
=            .add_systems(Update, spawn_districts)
=            .add_systems(Update, rollback);
=    }
index 9561877..618bea6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,7 @@ mod roads;
=mod sun;
=
=use bevy::prelude::*;
+use bevy::utils::HashSet;
=use bevy_egui::EguiPlugin;
=use buildings::BuildingsPlugin;
=use date::Date;
@@ -21,6 +22,7 @@ use districts::DistrictsPlugin;
=use explore::ExplorePlugin;
=use ground::GroundPlugin;
=use history::HistoryPlugin;
+use roads::RoadsPlugin;
=use sun::SunPlugin;
=
=const BEGINNING: i32 = 1860;
@@ -41,13 +43,15 @@ fn main() {
=            land_radius: 2000.,
=            sun_gap: 100.,
=        })
+        .init_resource::<PreloadedAssets>()
=        .init_state::<GameState>()
=        .add_plugins(camera::CameraPlugin(camera::CameraSettings {
-            elevation: 1200.,
-            distance: 1500.,
-            rps: 0.01,
+            elevation: 500.,
+            distance: 200.,
+            rps: 0.001,
=        }))
=        .add_plugins(GroundPlugin)
+        .add_plugins(RoadsPlugin)
=        .add_plugins(DistrictsPlugin)
=        .add_plugins(BuildingsPlugin)
=        .add_plugins(DatePlugin)
@@ -56,6 +60,7 @@ fn main() {
=        .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)
=        .add_systems(OnEnter(GameState::Explore), setup_exploration)
=        .run()
@@ -74,10 +79,29 @@ fn greet() {
=#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
=enum GameState {
=    #[default]
+    Loading,
=    Simulate,
=    Explore,
=}
=
+#[derive(Resource, Default, Debug)]
+struct PreloadedAssets(HashSet<UntypedHandle>);
+
+fn preload_assets(
+    server: Res<AssetServer>,
+    mut state: ResMut<NextState<GameState>>,
+    assets: Res<PreloadedAssets>,
+) {
+    let count = assets.0.len();
+
+    info!("Waiting for {count} assets to preload",);
+    let mut ids = assets.0.iter().map(|handle| handle.id());
+    if ids.all(|id| server.is_loaded_with_dependencies(id)) {
+        info!("All {count} assets preloaded!");
+        state.set(GameState::Simulate);
+    }
+}
+
=fn switch_states(
=    mut new_month_events: EventReader<NewMonth>,
=    date: Res<Date>,
index 2d243e7..19d5018 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,26 +1,163 @@
+use crate::{GameState, PreloadedAssets};
+use bevy::gltf::Gltf;
+use bevy::prelude::*;
+use bevy::utils::HashMap;
=use core::fmt;
=use std::fmt::Display;
=use std::ops::Not;
=
-#[derive(Debug, Default, PartialEq)]
-struct Road {
+pub struct RoadsPlugin;
+
+impl Plugin for RoadsPlugin {
+    fn build(&self, app: &mut App) {
+        app.init_resource::<Roads>()
+            .add_systems(Startup, setup)
+            .add_systems(OnEnter(GameState::Simulate), lay_initial_roads);
+    }
+}
+
+#[derive(Resource)]
+struct RoadAssets(Handle<Gltf>);
+
+fn setup(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);
+    preloaded.0.insert(handle.clone().untyped());
+    commands.insert_resource(RoadAssets(handle));
+}
+
+fn lay_initial_roads(world: &mut World) {
+    info!("Laying initial roads.");
+    let add_sections = world.register_system(add_section);
+
+    // 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)
+            };
+
+            world
+                .run_system_with_input(add_sections, (Coordinates(x, y * 10), new_section))
+                .unwrap();
+        }
+    }
+
+    // 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)
+            };
+            world
+                .run_system_with_input(add_sections, (Coordinates(x * 6, y), new_section))
+                .unwrap();
+        }
+    }
+}
+
+fn add_section(
+    In((coordinates, new_section)): In<(Coordinates, RoadSection)>,
+    mut commands: Commands,
+    road_assets: Res<RoadAssets>,
+    assets: Res<Assets<Gltf>>,
+    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 => {
+                let entity = commands
+                    .spawn(SceneBundle {
+                        scene: gltf.scenes[new_section.scene_index()].clone(),
+                        transform: Transform::from_xyz(
+                            (coordinates.0 * 10) as f32,
+                            0.0,
+                            (coordinates.1 * 10) as f32,
+                        ),
+                        ..default()
+                    })
+                    .insert(new_section)
+                    .id();
+                info!("Laying a new road section {new_section:?} at {coordinates:?} ({entity:?})");
+
+                roads.sections.insert(coordinates, entity);
+            }
+            Some(entity) => {
+                info!(
+                        "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();
+                commands.entity(*entity).despawn();
+
+                let entity = commands
+                    .spawn(SceneBundle {
+                        scene: gltf.scenes[section.scene_index()].clone(),
+                        transform: Transform::from_xyz(
+                            (coordinates.0 * 10) as f32,
+                            0.0,
+                            (coordinates.1 * 10) as f32,
+                        ),
+                        ..default()
+                    })
+                    .insert(section)
+                    .id();
+                info!("Combined road section {section:?} at {coordinates:?} ({entity:?})");
+
+                roads.sections.insert(coordinates, entity);
+            }
+        }
+    };
+}
+
+#[derive(Resource, Default)]
+pub struct Roads {
+    sections: HashMap<Coordinates, Entity>,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, Hash)]
+struct Coordinates(i32, i32);
+
+#[derive(Debug, Clone, Copy, Default, PartialEq, Component)]
+struct RoadSection {
=    directions: Directions,
=    sides: Sides,
=}
=
-impl Road {
+impl RoadSection {
=    pub fn can_combine(&self, other: &Self) -> bool {
=        self.sides.overlaps(&other.sides).not()
=    }
=
-    pub fn combine(mut self, other: &Self) -> Option<Self> {
+    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: directions,
-                sides,
-            })
+            Some(Self { directions, sides })
=        } else {
=            None
=        }
@@ -37,6 +174,10 @@ impl Road {
=        self.sides = self.sides.combine(&side);
=        self
=    }
+
+    pub fn scene_index(&self) -> usize {
+        self.directions.into()
+    }
=}
=
=#[derive(Copy, Clone, PartialEq, Eq)]
@@ -55,7 +196,7 @@ pub enum Quadrant {
=    WestNorth = 0b1000,
=}
=
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Clone, Copy)]
=struct Directions(u8);
=
=impl From<Direction> for Directions {
@@ -116,9 +257,15 @@ impl From<Directions> for u8 {
=    }
=}
=
+impl From<Directions> for usize {
+    fn from(value: Directions) -> Self {
+        value.0 as Self
+    }
+}
+
=impl fmt::Debug for Directions {
=    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "Sides({})", self.to_string())
+        write!(f, "Directions({})", self.to_string())
=    }
=}
=
@@ -166,8 +313,6 @@ impl Display for Directions {
=    }
=}
=
-// struct Sides(u8);
-
=#[cfg(test)]
=mod roads_tests {
=    use super::*;
@@ -194,7 +339,7 @@ mod roads_tests {
=
=// SIDES
=
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Clone, Copy)]
=struct Sides(u8);
=
=impl Sides {

Add simple camera controls

On by Tad Lispy

Via Plonq/bevy_panorbit_camera. I think eventually we will want our own camera movement system, but it's already much better than what we had before.

index 64c3233..34e3f8d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -742,6 +742,15 @@ dependencies = [
= "glam",
=]
=
+[[package]]
+name = "bevy_panorbit_camera"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28244cb96f651b603cb98b4b58145939ebdb362331347a827849750d3b5bc99f"
+dependencies = [
+ "bevy",
+]
+
=[[package]]
=name = "bevy_pbr"
=version = "0.13.0"
@@ -2861,6 +2870,7 @@ version = "0.1.0"
=dependencies = [
= "bevy",
= "bevy_egui",
+ "bevy_panorbit_camera",
= "derive_more",
= "itertools",
= "js-sys",
index 6670181..5138a14 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ edition = "2021"
=[dependencies]
=bevy = { version = "0.13", features = ["wayland"] }
=bevy_egui = "0.25.0"
+bevy_panorbit_camera = "0.16.0"
=derive_more = "0.99.17"
=itertools = "0.12.1"
=js-sys = "0.3.68"
index f466363..ff80706 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -1,43 +1,45 @@
=use bevy::prelude::*;
-use std::f32::consts::PI;
+use bevy_panorbit_camera::PanOrbitCamera;
+use bevy_panorbit_camera::PanOrbitCameraPlugin;
+use std::f32::consts::TAU;
=
-#[derive(Resource, Clone, Copy)]
-pub struct CameraSettings {
-    pub elevation: f32,
-    pub distance: f32,
-    pub rps: f32,
-}
-
-pub struct CameraPlugin(pub CameraSettings);
+pub struct CameraPlugin;
=
=impl Plugin for CameraPlugin {
=    fn build(&self, app: &mut App) {
-        let settings = self.0.clone();
-
-        app.insert_resource(settings)
+        app.add_plugins(PanOrbitCameraPlugin)
=            .add_systems(Startup, setup_camera)
-            .add_systems(Update, orbit_camera);
+            .add_systems(Update, limit_camera);
=    }
=}
=
-fn setup_camera(mut commands: Commands, settings: Res<CameraSettings>) {
-    commands.spawn(Camera3dBundle {
-        transform: Transform::from_xyz(settings.distance, settings.elevation, 0.)
-            .looking_at(Vec3::ZERO, Vec3::Y),
-        ..default()
-    });
+fn setup_camera(mut commands: Commands) {
+    commands
+        .spawn(Camera3dBundle {
+            // transform: Transform::from_xyz(settings.distance, settings.elevation, 0.)
+            //     .looking_at(Vec3::ZERO, Vec3::Y),
+            ..default()
+        })
+        .insert(PanOrbitCamera {
+            alpha: Some(0.0),
+            beta: Some(1.0),
+            focus: Vec3::ZERO,
+            radius: Some(1000.),
+            zoom_lower_limit: Some(500.),
+            zoom_upper_limit: Some(2000.),
+            beta_lower_limit: Some(TAU / 128.0),
+            ..default()
+        });
=}
=
-fn orbit_camera(
-    mut camera: Query<&mut Transform, With<Camera>>,
-    time: Res<Time>,
-    settings: Res<CameraSettings>,
-) {
-    let velocity = PI * 2.0 * settings.rps;
-
-    let mut transform = camera.single_mut();
-    transform.rotate_around(
-        Vec3::ZERO,
-        Quat::from_rotation_y(time.delta_seconds() * velocity),
-    )
+// TODO: Find a better way to lock camera focus on the ground.  Current solution
+// is a kludge that doesn't work very well, esp. when the camera is low.
+// Probably disable default panning system and instead control focus by mapping
+// pointer movements onto ground plane and inversing it. Part of the solution
+// might be here:
+// https://bevyengine.org/examples/3D%20Rendering/3d-viewport-to-world/
+fn limit_camera(mut cameras: Query<&mut PanOrbitCamera>) {
+    for mut camera in cameras.iter_mut() {
+        camera.target_focus.y = 0.0;
+    }
=}
index 618bea6..ef2cd9c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,6 +14,7 @@ use bevy::prelude::*;
=use bevy::utils::HashSet;
=use bevy_egui::EguiPlugin;
=use buildings::BuildingsPlugin;
+use camera::CameraPlugin;
=use date::Date;
=use date::DatePlugin;
=use date::NewMonth;
@@ -45,11 +46,7 @@ fn main() {
=        })
=        .init_resource::<PreloadedAssets>()
=        .init_state::<GameState>()
-        .add_plugins(camera::CameraPlugin(camera::CameraSettings {
-            elevation: 500.,
-            distance: 200.,
-            rps: 0.001,
-        }))
+        .add_plugins(CameraPlugin)
=        .add_plugins(GroundPlugin)
=        .add_plugins(RoadsPlugin)
=        .add_plugins(DistrictsPlugin)

Reintroduce districts, now with coordinates system

On by Tad Lispy

The Coordinates and Directions types were extracted from roads module to a new coordinates module, as they are now used in both roads and districts.

Districts are now described by a pair of coordinates (origin and extent), in preparation for placement system using the AABB method. There are already some basic tests around it. Basically a district is an axis aligned rectangle in a 2d integer space. That's how it's also described in HistoricalEvent type.

I made a new, simplified district model (district_300x180_b). It only has ground and a small cube to mark it's corner nearest the origin (so I can see how it's rotated).

new file mode 100644
index 0000000..fa9d59a
Binary files /dev/null and b/art/district_300x180_b.blend differ
new file mode 100644
index 0000000..f09098e
Binary files /dev/null and b/assets/district_300x180_b.glb differ
index a2369f5..beb220d 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -8,11 +8,12 @@ pub struct BuildingsPlugin;
=impl Plugin for BuildingsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<ConstructionOrder>()
-            .add_systems(
-                Update,
-                order_construction.run_if(in_state(GameState::Simulate)),
-            )
-            // .add_systems(Update, spawn_buildings)
+            // Temporarily disabled to make the roads visible
+            // .add_systems(
+            //     Update,
+            //     order_construction.run_if(in_state(GameState::Simulate)),
+            // )
+            .add_systems(Update, spawn_buildings)
=            .add_systems(Update, rollback);
=    }
=}
new file mode 100644
index 0000000..8a37843
--- /dev/null
+++ b/src/coordinates.rs
@@ -0,0 +1,57 @@
+use std::fmt::Display;
+
+use bevy::math::Vec3;
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
+pub struct Coordinates(pub i32, pub i32);
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Direction {
+    North = 0b0001,
+    East = 0b0010,
+    South = 0b0100,
+    West = 0b1000,
+}
+
+impl Coordinates {
+    pub fn shift(mut self, distance: i32, direction: Direction) -> Self {
+        match direction {
+            Direction::North => self.1 -= distance,
+            Direction::East => self.1 += distance,
+            Direction::South => self.1 += distance,
+            Direction::West => self.1 -= distance,
+        };
+        self
+    }
+}
+
+impl Display for Coordinates {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let Coordinates(x, z) = self;
+        write!(f, "({x}, {z})")
+    }
+}
+
+impl From<Coordinates> for Vec3 {
+    fn from(coordinates: Coordinates) -> Self {
+        let Coordinates(x, z) = coordinates;
+        Self::new((x * 10) as f32, 0.0, (z * 10) as f32)
+    }
+}
+
+#[cfg(test)]
+mod coordinates_tests {
+    use super::*;
+
+    #[test]
+    fn default() {
+        let mut a = Coordinates::default();
+        assert_eq!(a, Coordinates(0, 0));
+    }
+
+    #[test]
+    fn shifting() {
+        let mut a = Coordinates::default();
+        assert_eq!(a, Coordinates(0, 0));
+    }
+}
index a39a829..687fb79 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,6 +1,8 @@
+use crate::coordinates::{Coordinates, Direction};
=use crate::date::NewMonth;
=use crate::history::TimeTravelRequest;
=use crate::GameState;
+use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=
=pub struct DistrictsPlugin;
@@ -8,12 +10,12 @@ pub struct DistrictsPlugin;
=impl Plugin for DistrictsPlugin {
=    fn build(&self, app: &mut App) {
=        app.add_event::<NewDistrictEstablished>()
-            // Temporarily disabled to make the roads visible
-            // .add_systems(
-            //     Update,
-            //     establish_new_districts.run_if(in_state(GameState::Simulate)),
-            // )
-            .add_systems(Update, spawn_districts)
+            .add_systems(Startup, setup_spawn_district)
+            .add_systems(
+                Update,
+                establish_new_districts.run_if(in_state(GameState::Simulate)),
+            )
+            .add_systems(Update, spawn_new_districts)
=            .add_systems(Update, rollback);
=    }
=}
@@ -22,16 +24,14 @@ impl Plugin for DistrictsPlugin {
=///
=/// It will be stored in the history, and will result in spawning a new building.
=#[derive(Event, Clone, Copy, Debug)]
-pub struct NewDistrictEstablished {
-    pub x: f32,
-    pub z: f32,
-}
+pub struct NewDistrictEstablished(pub District);
=
=/// A tag for district entities
-#[derive(Component)]
-pub struct District;
-
-const MODEL_PATH: &str = "neighbourhood_300x180_A.glb#Scene0";
+#[derive(Component, Debug, Clone, Copy)]
+pub struct District {
+    pub origin: Coordinates,
+    pub extent: Coordinates,
+}
=
=fn establish_new_districts(
=    mut new_month_events: EventReader<NewMonth>,
@@ -45,57 +45,98 @@ fn establish_new_districts(
=    let count = buildings.into_iter().count();
=
=    const ROWS: usize = 6;
-    const DISTANCE: usize = 300;
-    let x = (count.rem_euclid(ROWS) * DISTANCE) as f32;
-    let z = (count / ROWS * DISTANCE) as f32;
+    const WIDTH: usize = 18;
+    const BREDTH: usize = 30;
+
+    let x = (count.rem_euclid(ROWS) * WIDTH) as i32;
+    let y = (count / ROWS * BREDTH) as i32;
=
-    info!("New district established at ({x:.2}, {z:.2})!");
+    let origin = Coordinates(x, y);
+    let extent = origin
+        .clone()
+        .shift(30, Direction::East)
+        .shift(18, Direction::South);
=
-    new_districts.send(NewDistrictEstablished { x, z });
+    let district = District { origin, extent };
+    info!("New district established: {district:?}");
+
+    new_districts.send(NewDistrictEstablished(district));
=}
=
-fn spawn_districts(
+fn spawn_new_districts(
=    mut commands: Commands,
-    assets: ResMut<AssetServer>,
=    mut new_districts: EventReader<NewDistrictEstablished>,
+    spawn_district: Res<SpawnDistrict>,
=) {
-    for NewDistrictEstablished { x, z } in new_districts.read() {
-        let model = assets.load(MODEL_PATH);
-
-        info!("Spawning a new district at ({x:.2}, {z:.2})!");
-
-        commands
-            .spawn(SceneBundle {
-                scene: model,
-                transform: Transform::from_xyz(*x, 4., *z),
-                ..default()
-            })
-            .insert(District);
+    for NewDistrictEstablished(district) in new_districts.read() {
+        commands.run_system_with_input(spawn_district.0, district.to_owned());
=    }
=}
=
=fn rollback(
=    mut requests: EventReader<TimeTravelRequest>,
=    districts: Query<Entity, With<District>>,
-    assets: ResMut<AssetServer>,
=    mut commands: Commands,
+    spawn_district: Res<SpawnDistrict>,
=) {
=    for TimeTravelRequest(snapshot) in requests.read() {
=        for entity in districts.iter() {
=            commands.entity(entity).despawn_recursive();
=        }
=
-        for transform in snapshot.clone().districts {
-            // TODO: DRY with spawn_buildings
-            let model = assets.load(MODEL_PATH);
-
-            commands
-                .spawn(SceneBundle {
-                    scene: model,
-                    transform: transform.clone(),
-                    ..default()
-                })
-                .insert(District);
+        for district in snapshot.clone().districts {
+            commands.run_system_with_input(spawn_district.0, district.to_owned());
=        }
=    }
=}
+
+// Spawn district is a one-shot system attached to a resource. It does what it says on the tin.
+
+#[derive(Resource, Debug)]
+struct SpawnDistrict(SystemId<District>);
+
+fn setup_spawn_district(world: &mut World) {
+    let system = world.register_system(spawn_district);
+    world.insert_resource(SpawnDistrict(system));
+}
+
+const MODEL_PATH: &str = "district_300x180_b.glb#Scene0";
+
+fn spawn_district(In(district): In<District>, assets: ResMut<AssetServer>, mut commands: Commands) {
+    info!("Spawning a district: {district:?}");
+
+    let model = assets.load(MODEL_PATH);
+    commands
+        .spawn(SceneBundle {
+            scene: model,
+            transform: Transform::from_translation(district.origin.into()),
+            ..default()
+        })
+        .insert(district);
+}
+
+#[cfg(test)]
+mod district_tests {
+    use std::ops::Not;
+
+    use super::*;
+    use bevy::math::Rect;
+
+    #[test]
+    fn overlapping() {
+        // Separate
+        let a = Rect::new(0., 0., 1., 1.);
+        let b = Rect::new(2., 2., 3., 3.);
+        assert!(a.intersect(b).is_empty());
+
+        // Overlapping corners
+        let a = Rect::new(0., 0., 2., 2.);
+        let b = Rect::new(1., 1., 3., 3.);
+        assert!(a.intersect(b).is_empty().not());
+
+        // Touching
+        let a = Rect::new(0., 0., 2., 2.);
+        let b = Rect::new(2., 0., 4., 2.);
+        assert!(a.intersect(b).is_empty());
+    }
+}
index 272697d..c536fad 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -68,8 +68,8 @@ impl Display for HistoricalEvent {
=                    "construction of a new building ordered at ({x:.2}, {z:.2})",
=                )
=            }
-            HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished { x, z }) => {
-                write!(f, "New district established at ({x:.2}, {z:.2})",)
+            HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {
+                write!(f, "New district established: {district:?}",)
=            }
=        }
=    }
@@ -102,7 +102,7 @@ fn register_historical_events(
=pub struct Snapshot {
=    pub date: DayMonth,
=    pub buildings: Vec<Transform>,
-    pub districts: Vec<Transform>,
+    pub districts: Vec<District>,
=}
=
=fn take_snapshot(
@@ -110,7 +110,7 @@ fn take_snapshot(
=    date: Res<Date>,
=    mut history: ResMut<History>,
=    buildings: Query<&Transform, With<Building>>,
-    districts: Query<&Transform, With<District>>,
+    districts: Query<&District>,
=) {
=    if new_month.read().count() == 0 {
=        return;
@@ -118,7 +118,7 @@ fn take_snapshot(
=
=    let timestamp: Timestamp = date.0.into();
=    let buildings = buildings.iter().map(|transform| *transform).collect();
-    let districts = districts.iter().map(|transform| *transform).collect();
+    let districts = districts.iter().map(|district| *district).collect();
=    let snapshot = Snapshot {
=        date: date.0,
=        buildings,
index ef2cd9c..668e211 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
=mod buildings;
=mod camera;
+mod coordinates;
=mod date;
=mod day_month;
=mod districts;
index 431e980..61d4417 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -21,6 +21,7 @@ impl Display for History {
=        writeln!(f, "   coordinates point not null")?;
=        writeln!(f, ");")?;
=
+        #[allow(clippy::zero_prefixed_literal)]
=        for event in self.events.iter() {
=            let date = event.date;
=            let year = date.year();
@@ -44,7 +45,7 @@ impl Display for History {
=                    writeln!(f, ");")?;
=                }
=                crate::history::HistoricalEvent::NewDistrictEstablished(
-                    NewDistrictEstablished { x, z },
+                    NewDistrictEstablished(district),
=                ) => {
=                    writeln!(f, "Insert into districts (")?;
=                    writeln!(f, "   date,")?;
@@ -54,7 +55,7 @@ impl Display for History {
=                        f,
=                        "   {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
=                    )?;
-                    writeln!(f, "   ({x:.3}, {z:.3})")?;
+                    writeln!(f, "   {coordinates}", coordinates = district.origin)?;
=                    writeln!(f, ");")?;
=                }
=            };
index 19d5018..1f1b3fb 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,3 +1,4 @@
+use crate::coordinates::{Coordinates, Direction};
=use crate::{GameState, PreloadedAssets};
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -139,9 +140,6 @@ pub struct Roads {
=    sections: HashMap<Coordinates, Entity>,
=}
=
-#[derive(Debug, Default, PartialEq, Eq, Hash)]
-struct Coordinates(i32, i32);
-
=#[derive(Debug, Clone, Copy, Default, PartialEq, Component)]
=struct RoadSection {
=    directions: Directions,
@@ -180,14 +178,6 @@ impl RoadSection {
=    }
=}
=
-#[derive(Copy, Clone, PartialEq, Eq)]
-pub enum Direction {
-    North = 0b0001,
-    East = 0b0010,
-    South = 0b0100,
-    West = 0b1000,
-}
-
=#[derive(Copy, Clone, PartialEq, Eq)]
=pub enum Quadrant {
=    NorthEast = 0b0001,

Add Clippy to Rust toolchain

On by Tad Lispy

index 571e68f..e6ede63 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,5 +1,5 @@
=[toolchain]
=channel = "stable"
-components = [ "rust-src" ]
+components = [ "rust-src", "clippy" ]
=targets = [ "wasm32-unknown-unknown" ]
=profile = "default"

Create a check goal in the Makefile

On by Tad Lispy

It runs Clippy after every change to the source code.

index 493e07b..afd9576 100644
--- a/Makefile
+++ b/Makefile
@@ -41,6 +41,11 @@ serve: web
=	miniserve --interfaces=127.0.0.1 --index=index.html web
=.PHONY: serve
=
+check: ## Check and recheck the code after every change
+check:
+	watchexec --watch src/ --debounce 1s cargo clippy
+.PHONY: check
+
=help: ## Print this help message
=help:
=	@
index 5795f21..8f0c776 100644
--- a/flake.nix
+++ b/flake.nix
@@ -46,6 +46,7 @@
=            pkgs.rust-analyzer
=            pkgs.miniserve
=            pkgs.jq
+            pkgs.watchexec
=          ];
=          project_name = project-name; # Expose as an environment variable for make
=          LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ with pkgs; lib.makeLibraryPath buildInputs }";

Fix Coordinates::shift method

On by Tad Lispy

index 8a37843..075c95f 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -17,9 +17,9 @@ impl Coordinates {
=    pub fn shift(mut self, distance: i32, direction: Direction) -> Self {
=        match direction {
=            Direction::North => self.1 -= distance,
-            Direction::East => self.1 += distance,
+            Direction::East => self.0 += distance,
=            Direction::South => self.1 += distance,
-            Direction::West => self.1 -= distance,
+            Direction::West => self.0 -= distance,
=        };
=        self
=    }

When creating districts, create roads around them

On by Tad Lispy

Reorder the width and height in the name of district file (first width, then height).

similarity index 62%
rename from art/district_300x180_b.blend
rename to art/district_180x300_b.blend
index fa9d59a..00c1e1b 100644
Binary files a/art/district_300x180_b.blend and b/art/district_180x300_b.blend differ
new file mode 100644
index 0000000..be5d464
Binary files /dev/null and b/assets/district_180x300_b.glb differ
deleted file mode 100644
index f09098e..0000000
Binary files a/assets/district_300x180_b.glb and /dev/null differ
index 075c95f..452cc7e 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -2,6 +2,8 @@ use std::fmt::Display;
=
=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)]
=pub struct Coordinates(pub i32, pub i32);
=
index 687fb79..8b92ee4 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,9 +1,11 @@
=use crate::coordinates::{Coordinates, Direction};
=use crate::date::NewMonth;
=use crate::history::TimeTravelRequest;
+use crate::roads::{RoadSection, SpawnRoadSection};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
+use bevy::utils::HashSet;
=
=pub struct DistrictsPlugin;
=
@@ -33,6 +35,102 @@ pub struct District {
=    pub extent: Coordinates,
=}
=
+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..=easternmost;
+        let latitudinal_range = northernmost..=southernmost;
+
+        // TODO: Extract into a helper function in roads module
+        let northern_border = longitudinal_range.clone().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(longitude, northernmost), section)
+        });
+        let southern_border = longitudinal_range.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(longitude, southernmost), section)
+        });
+        let eastern_border = latitudinal_range.clone().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(easternmost, latitude), section)
+        });
+        let western_border = latitudinal_range.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(westernmost, latitude), section)
+        });
+
+        let borders = northern_border
+            .chain(southern_border)
+            .chain(eastern_border)
+            .chain(western_border);
+        HashSet::from_iter(borders)
+    }
+
+    fn south(&self) -> i32 {
+        self.origin.1.max(self.extent.1)
+    }
+
+    fn east(&self) -> i32 {
+        self.origin.0.max(self.extent.0)
+    }
+
+    fn north(&self) -> i32 {
+        self.origin.1.min(self.extent.1)
+    }
+
+    fn west(&self) -> i32 {
+        self.origin.0.min(self.extent.0)
+    }
+}
+
=fn establish_new_districts(
=    mut new_month_events: EventReader<NewMonth>,
=    mut new_districts: EventWriter<NewDistrictEstablished>,
@@ -44,7 +142,7 @@ fn establish_new_districts(
=
=    let count = buildings.into_iter().count();
=
-    const ROWS: usize = 6;
+    const ROWS: usize = 5;
=    const WIDTH: usize = 18;
=    const BREDTH: usize = 30;
=
@@ -54,8 +152,8 @@ fn establish_new_districts(
=    let origin = Coordinates(x, y);
=    let extent = origin
=        .clone()
-        .shift(30, Direction::East)
-        .shift(18, Direction::South);
+        .shift(18, Direction::East)
+        .shift(30, Direction::South);
=
=    let district = District { origin, extent };
=    info!("New district established: {district:?}");
@@ -102,7 +200,12 @@ fn setup_spawn_district(world: &mut World) {
=
=const MODEL_PATH: &str = "district_300x180_b.glb#Scene0";
=
-fn spawn_district(In(district): In<District>, assets: ResMut<AssetServer>, mut commands: Commands) {
+fn spawn_district(
+    In(district): In<District>,
+    assets: ResMut<AssetServer>,
+    mut commands: Commands,
+    spawn_road_section: Res<SpawnRoadSection>,
+) {
=    info!("Spawning a district: {district:?}");
=
=    let model = assets.load(MODEL_PATH);
@@ -113,6 +216,10 @@ fn spawn_district(In(district): In<District>, assets: ResMut<AssetServer>, mut c
=            ..default()
=        })
=        .insert(district);
+
+    for section in district.border_roads() {
+        commands.run_system_with_input(spawn_road_section.0, section.to_owned());
+    }
=}
=
=#[cfg(test)]
index 1f1b3fb..5ca8ad7 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,5 +1,7 @@
=use crate::coordinates::{Coordinates, Direction};
-use crate::{GameState, PreloadedAssets};
+use crate::history::TimeTravelRequest;
+use crate::PreloadedAssets;
+use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
=use bevy::utils::HashMap;
@@ -13,7 +15,10 @@ impl Plugin for RoadsPlugin {
=    fn build(&self, app: &mut App) {
=        app.init_resource::<Roads>()
=            .add_systems(Startup, setup)
-            .add_systems(OnEnter(GameState::Simulate), lay_initial_roads);
+            .add_systems(Startup, setup_spawn_road_section)
+            .add_systems(Update, rollback)
+        // .add_systems(OnEnter(GameState::Simulate), lay_initial_roads)
+        ;
=    }
=}
=
@@ -28,9 +33,8 @@ fn setup(assets: Res<AssetServer>, mut commands: Commands, mut preloaded: ResMut
=    commands.insert_resource(RoadAssets(handle));
=}
=
-fn lay_initial_roads(world: &mut World) {
+fn lay_initial_roads(mut commands: Commands, spawn_road_section: Res<SpawnRoadSection>) {
=    info!("Laying initial roads.");
-    let add_sections = world.register_system(add_section);
=
=    // Longitudinal roads
=    let x_min = -12;
@@ -49,9 +53,8 @@ fn lay_initial_roads(world: &mut World) {
=                    .add_direction(&Direction::West)
=            };
=
-            world
-                .run_system_with_input(add_sections, (Coordinates(x, y * 10), new_section))
-                .unwrap();
+            commands
+                .run_system_with_input(spawn_road_section.0, (Coordinates(x, y * 10), new_section));
=        }
=    }
=
@@ -71,13 +74,22 @@ fn lay_initial_roads(world: &mut World) {
=                    .add_direction(&Direction::South)
=                    .add_direction(&Direction::North)
=            };
-            world
-                .run_system_with_input(add_sections, (Coordinates(x * 6, y), new_section))
-                .unwrap();
+            commands
+                .run_system_with_input(spawn_road_section.0, (Coordinates(x * 6, y), new_section));
=        }
=    }
=}
=
+// Spawn district is a one-shot system attached to a resource. It does what it says on the tin.
+
+#[derive(Resource, Debug)]
+pub struct SpawnRoadSection(pub SystemId<(Coordinates, RoadSection)>);
+
+fn setup_spawn_road_section(world: &mut World) {
+    let system = world.register_system(add_section);
+    world.insert_resource(SpawnRoadSection(system));
+}
+
=fn add_section(
=    In((coordinates, new_section)): In<(Coordinates, RoadSection)>,
=    mut commands: Commands,
@@ -113,8 +125,9 @@ fn add_section(
=                    );
=                let old_section = sections.get_mut(*entity).unwrap();
=                let section = old_section.combine(&new_section).unwrap();
-                commands.entity(*entity).despawn();
=
+                // re-spawn new entuty
+                commands.entity(*entity).despawn_recursive();
=                let entity = commands
=                    .spawn(SceneBundle {
=                        scene: gltf.scenes[section.scene_index()].clone(),
@@ -135,13 +148,27 @@ fn add_section(
=    };
=}
=
+fn rollback(
+    mut requests: EventReader<TimeTravelRequest>,
+    sections: Query<Entity, With<RoadSection>>,
+    mut roads: ResMut<Roads>,
+    mut commands: Commands,
+) {
+    for TimeTravelRequest(snapshot) in requests.read() {
+        for entity in sections.iter() {
+            commands.entity(entity).despawn_recursive();
+        }
+        roads.sections.clear();
+        // TODO: Re-lay initial roads
+    }
+}
=#[derive(Resource, Default)]
=pub struct Roads {
=    sections: HashMap<Coordinates, Entity>,
=}
=
-#[derive(Debug, Clone, Copy, Default, PartialEq, Component)]
-struct RoadSection {
+#[derive(Debug, Clone, Copy, Default, PartialEq, Component, Hash, Eq)]
+pub struct RoadSection {
=    directions: Directions,
=    sides: Sides,
=}
@@ -178,7 +205,7 @@ impl RoadSection {
=    }
=}
=
-#[derive(Copy, Clone, PartialEq, Eq)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash)]
=pub enum Quadrant {
=    NorthEast = 0b0001,
=    EastSouth = 0b0010,
@@ -186,7 +213,7 @@ pub enum Quadrant {
=    WestNorth = 0b1000,
=}
=
-#[derive(PartialEq, Eq, Clone, Copy)]
+#[derive(PartialEq, Eq, Clone, Copy, Hash)]
=struct Directions(u8);
=
=impl From<Direction> for Directions {
@@ -329,7 +356,7 @@ mod roads_tests {
=
=// SIDES
=
-#[derive(PartialEq, Eq, Clone, Copy)]
+#[derive(PartialEq, Eq, Clone, Copy, Hash)]
=struct Sides(u8);
=
=impl Sides {

Introduce Latitude and Longitude types

On by Tad Lispy

For safety, and also to extract the range logic there. It was more work than I thought.

index 452cc7e..930dc7f 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -1,11 +1,18 @@
+use derive_more::{AsRef, From, Into};
=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)]
-pub struct Coordinates(pub i32, pub i32);
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, AsRef)]
+pub struct Coordinates(Longitude, Latitude);
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, From, Into)]
+pub struct Latitude(i32);
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, From, Into)]
+pub struct Longitude(i32);
=
=#[derive(Copy, Clone, PartialEq, Eq)]
=pub enum Direction {
@@ -16,7 +23,7 @@ pub enum Direction {
=}
=
=impl Coordinates {
-    pub fn shift(mut self, distance: i32, direction: Direction) -> Self {
+    pub fn shift(&mut self, distance: i32, direction: Direction) -> &mut Self {
=        match direction {
=            Direction::North => self.1 -= distance,
=            Direction::East => self.0 += distance,
@@ -25,19 +32,75 @@ impl Coordinates {
=        };
=        self
=    }
+
+    pub fn longitude(&self) -> Longitude {
+        self.0
+    }
+
+    pub fn latitude(&self) -> Latitude {
+        self.1
+    }
+
+    pub fn new(longitude: &Longitude, latitude: &Latitude) -> Self {
+        Self(longitude.clone(), latitude.clone())
+    }
=}
=
-impl Display for Coordinates {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let Coordinates(x, z) = self;
-        write!(f, "({x}, {z})")
+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)
+    }
+}
+
+impl AddAssign<i32> for Latitude {
+    fn add_assign(&mut self, rhs: i32) {
+        self.0 += rhs;
+    }
+}
+
+impl SubAssign<i32> for Latitude {
+    fn sub_assign(&mut self, rhs: i32) {
+        self.0 -= rhs;
=    }
=}
=
-impl From<Coordinates> for Vec3 {
-    fn from(coordinates: Coordinates) -> Self {
-        let Coordinates(x, z) = coordinates;
-        Self::new((x * 10) as f32, 0.0, (z * 10) as f32)
+impl AddAssign<i32> for Longitude {
+    fn add_assign(&mut self, rhs: i32) {
+        self.0 += rhs;
+    }
+}
+
+impl SubAssign<i32> for Longitude {
+    fn sub_assign(&mut self, rhs: i32) {
+        self.0 -= rhs;
+    }
+}
+
+impl Latitude {
+    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()
+    }
+}
+
+impl Longitude {
+    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()
+    }
+}
+
+impl Display for Coordinates {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let Coordinates(Longitude(x), Latitude(z)) = self;
+        write!(f, "({x}, {z})")
=    }
=}
=
@@ -47,13 +110,14 @@ mod coordinates_tests {
=
=    #[test]
=    fn default() {
-        let mut a = Coordinates::default();
-        assert_eq!(a, Coordinates(0, 0));
+        let a = Coordinates::default();
+        assert_eq!(a, Coordinates::new(&Longitude::from(0), &Latitude::from(0)));
=    }
=
=    #[test]
=    fn shifting() {
=        let mut a = Coordinates::default();
-        assert_eq!(a, Coordinates(0, 0));
+        a.shift(4, Direction::East).shift(5, Direction::South);
+        assert_eq!(a, Coordinates::new(&Longitude::from(4), &Latitude::from(5)));
=    }
=}
index 8b92ee4..9c136a4 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,4 +1,4 @@
-use crate::coordinates::{Coordinates, Direction};
+use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
=use crate::history::TimeTravelRequest;
=use crate::roads::{RoadSection, SpawnRoadSection};
@@ -6,6 +6,7 @@ use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::HashSet;
+use std::borrow::Borrow;
=
=pub struct DistrictsPlugin;
=
@@ -42,15 +43,15 @@ impl District {
=        let easternmost = self.east();
=        let southernmost = self.south();
=
-        let longitudinal_range = westernmost..=easternmost;
-        let latitudinal_range = northernmost..=southernmost;
+        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.clone().map(|longitude| {
-            let section = if longitude == westernmost {
+        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 {
+            } else if longitude == &easternmost {
=                // End of the road
=                RoadSection::default().add_direction(&Direction::West)
=            } else {
@@ -59,13 +60,14 @@ impl District {
=                    .add_direction(&Direction::East)
=                    .add_direction(&Direction::West)
=            };
-            (Coordinates(longitude, northernmost), section)
+            (Coordinates::new(longitude, &northernmost), section)
=        });
-        let southern_border = longitudinal_range.map(|longitude| {
-            let section = if longitude == westernmost {
+
+        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 {
+            } else if *longitude == easternmost {
=                // End of the road
=                RoadSection::default().add_direction(&Direction::West)
=            } else {
@@ -74,13 +76,13 @@ impl District {
=                    .add_direction(&Direction::East)
=                    .add_direction(&Direction::West)
=            };
-            (Coordinates(longitude, southernmost), section)
+            (Coordinates::new(&longitude, &southernmost), section)
=        });
-        let eastern_border = latitudinal_range.clone().map(|latitude| {
-            let section = if latitude == northernmost {
+        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 {
+            } else if *latitude == southernmost {
=                // End of the road
=                RoadSection::default().add_direction(&Direction::North)
=            } else {
@@ -89,13 +91,13 @@ impl District {
=                    .add_direction(&Direction::South)
=                    .add_direction(&Direction::North)
=            };
-            (Coordinates(easternmost, latitude), section)
+            (Coordinates::new(&easternmost, &latitude), section)
=        });
-        let western_border = latitudinal_range.map(|latitude| {
-            let section = if latitude == northernmost {
+        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 {
+            } else if *latitude == southernmost {
=                // End of the road
=                RoadSection::default().add_direction(&Direction::North)
=            } else {
@@ -104,7 +106,7 @@ impl District {
=                    .add_direction(&Direction::South)
=                    .add_direction(&Direction::North)
=            };
-            (Coordinates(westernmost, latitude), section)
+            (Coordinates::new(&westernmost, &latitude), section)
=        });
=
=        let borders = northern_border
@@ -114,20 +116,20 @@ impl District {
=        HashSet::from_iter(borders)
=    }
=
-    fn south(&self) -> i32 {
-        self.origin.1.max(self.extent.1)
+    fn south(&self) -> Latitude {
+        self.origin.latitude().max(self.extent.latitude())
=    }
=
-    fn east(&self) -> i32 {
-        self.origin.0.max(self.extent.0)
+    fn east(&self) -> Longitude {
+        self.origin.longitude().max(self.extent.longitude())
=    }
=
-    fn north(&self) -> i32 {
-        self.origin.1.min(self.extent.1)
+    fn north(&self) -> Latitude {
+        self.origin.latitude().min(self.extent.latitude())
=    }
=
-    fn west(&self) -> i32 {
-        self.origin.0.min(self.extent.0)
+    fn west(&self) -> Longitude {
+        self.origin.longitude().min(self.extent.longitude())
=    }
=}
=
@@ -149,11 +151,12 @@ fn establish_new_districts(
=    let x = (count.rem_euclid(ROWS) * WIDTH) as i32;
=    let y = (count / ROWS * BREDTH) as i32;
=
-    let origin = Coordinates(x, y);
+    let origin = Coordinates::new(&Longitude::from(x), &Latitude::from(y));
=    let extent = origin
=        .clone()
=        .shift(18, Direction::East)
-        .shift(30, Direction::South);
+        .shift(30, Direction::South)
+        .to_owned();
=
=    let district = District { origin, extent };
=    info!("New district established: {district:?}");
@@ -212,7 +215,7 @@ fn spawn_district(
=    commands
=        .spawn(SceneBundle {
=            scene: model,
-            transform: Transform::from_translation(district.origin.into()),
+            transform: Transform::from_translation(district.origin.borrow().into()),
=            ..default()
=        })
=        .insert(district);
index 5ca8ad7..c258bac 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -6,6 +6,7 @@ use bevy::gltf::Gltf;
=use bevy::prelude::*;
=use bevy::utils::HashMap;
=use core::fmt;
+use std::borrow::Borrow;
=use std::fmt::Display;
=use std::ops::Not;
=
@@ -37,47 +38,47 @@ fn lay_initial_roads(mut commands: Commands, spawn_road_section: Res<SpawnRoadSe
=    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 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));
+    //     }
+    // }
=}
=
=// Spawn district is a one-shot system attached to a resource. It does what it says on the tin.
@@ -106,11 +107,7 @@ fn add_section(
=                let entity = commands
=                    .spawn(SceneBundle {
=                        scene: gltf.scenes[new_section.scene_index()].clone(),
-                        transform: Transform::from_xyz(
-                            (coordinates.0 * 10) as f32,
-                            0.0,
-                            (coordinates.1 * 10) as f32,
-                        ),
+                        transform: Transform::from_translation(coordinates.borrow().into()),
=                        ..default()
=                    })
=                    .insert(new_section)
@@ -131,11 +128,7 @@ fn add_section(
=                let entity = commands
=                    .spawn(SceneBundle {
=                        scene: gltf.scenes[section.scene_index()].clone(),
-                        transform: Transform::from_xyz(
-                            (coordinates.0 * 10) as f32,
-                            0.0,
-                            (coordinates.1 * 10) as f32,
-                        ),
+                        transform: Transform::from_translation(coordinates.borrow().into()),
=                        ..default()
=                    })
=                    .insert(section)

Fix the district model path

On by Tad Lispy

index 9c136a4..943dcd9 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_300x180_b.glb#Scene0";
+const MODEL_PATH: &str = "district_180x300_b.glb#Scene0";
=
=fn spawn_district(
=    In(district): In<District>,

Finished first district with building zones and roads. Added 4m in height to road blocks. Elevated base block and set origin at bottom.

On by pedrolinux

new file mode 100644
index 0000000..65d936a
Binary files /dev/null and b/art/buildings.blend differ
new file mode 100644
index 0000000..63ebee6
Binary files /dev/null and b/art/models/pitsOfficeCorner.glb differ
new file mode 100644
index 0000000..f7f1abd
Binary files /dev/null and b/art/models/tentRoof.glb differ
index 9627e9b..6b4d184 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ

Added some kenney models to project folder in art/models. Added 5 scenes to buildings.blend with different building types. Exported to assets buildings.blend and neighbourhood_300x180_A.blend.

On by pedrolinux

index 65d936a..5bf4181 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differ
new file mode 100644
index 0000000..c6646e2
Binary files /dev/null and b/art/models/Textures/colormap.png differ
new file mode 100644
index 0000000..7256dcd
Binary files /dev/null and b/art/models/building-sample-house-a.glb differ
new file mode 100644
index 0000000..5bd30f0
Binary files /dev/null and b/art/models/building-sample-house-b.glb differ
new file mode 100644
index 0000000..c5effc4
Binary files /dev/null and b/art/models/building-sample-house-c.glb differ
new file mode 100644
index 0000000..e53e4b6
Binary files /dev/null and b/art/models/building-window-door-window.glb differ
new file mode 100644
index 0000000..23e0db8
Binary files /dev/null and b/art/models/roof-gable.glb differ
index 6b4d184..60827e8 100644
Binary files a/art/neighbourhood_300x180_A.blend and b/art/neighbourhood_300x180_A.blend differ
new file mode 100644
index 0000000..17c6b72
Binary files /dev/null and b/assets/buildings.glb differ
index 7948517..0d64f4a 100644
Binary files a/assets/neighbourhood_300x180_A.glb and b/assets/neighbourhood_300x180_A.glb differ

Added small readme.md with just the instructions to run nix (not instructions on how to setup nix in the first place yet)

On by pedrolinux

new file mode 100644
index 0000000..ba91b1d
--- /dev/null
+++ b/Readme.MD
@@ -0,0 +1,3 @@
+-   Run Bevy locally after setting up nix:
+    -   `nix develop`
+    -   `make web/develop` (default runs 8080)

Add f3d gltf viewer to the development environment

On by Tad Lispy

index 8f0c776..35fed79 100644
--- a/flake.nix
+++ b/flake.nix
@@ -47,6 +47,7 @@
=            pkgs.miniserve
=            pkgs.jq
=            pkgs.watchexec
+            pkgs.f3d
=          ];
=          project_name = project-name; # Expose as an environment variable for make
=          LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ with pkgs; lib.makeLibraryPath buildInputs }";

The Coordinates constructor will take ownership

On by Tad Lispy

...of the Longitude and Latitude components.

After learing more about Rust ownership system I came to think that in this case it's better to pass arguments by value and let the call site deide whether they should be cloned or not.

index 930dc7f..c720ffe 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -41,8 +41,8 @@ impl Coordinates {
=        self.1
=    }
=
-    pub fn new(longitude: &Longitude, latitude: &Latitude) -> Self {
-        Self(longitude.clone(), latitude.clone())
+    pub fn new(longitude: Longitude, latitude: Latitude) -> Self {
+        Self(longitude, latitude)
=    }
=}
=
@@ -111,13 +111,13 @@ mod coordinates_tests {
=    #[test]
=    fn default() {
=        let a = Coordinates::default();
-        assert_eq!(a, Coordinates::new(&Longitude::from(0), &Latitude::from(0)));
+        assert_eq!(a, Coordinates::new(Longitude::from(0), Latitude::from(0)));
=    }
=
=    #[test]
=    fn shifting() {
=        let mut a = Coordinates::default();
=        a.shift(4, Direction::East).shift(5, Direction::South);
-        assert_eq!(a, Coordinates::new(&Longitude::from(4), &Latitude::from(5)));
+        assert_eq!(a, Coordinates::new(Longitude::from(4), Latitude::from(5)));
=    }
=}
index 2ee674a..a33c72b 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -60,7 +60,7 @@ impl District {
=                    .add_direction(&Direction::East)
=                    .add_direction(&Direction::West)
=            };
-            (Coordinates::new(longitude, &northernmost), section)
+            (Coordinates::new(*longitude, northernmost), section)
=        });
=
=        let southern_border = longitudinal_range.iter().map(|longitude| {
@@ -76,7 +76,7 @@ impl District {
=                    .add_direction(&Direction::East)
=                    .add_direction(&Direction::West)
=            };
-            (Coordinates::new(&longitude, &southernmost), section)
+            (Coordinates::new(*longitude, southernmost), section)
=        });
=        let eastern_border = latitudinal_range.iter().map(|latitude| {
=            let section = if *latitude == northernmost {
@@ -91,7 +91,7 @@ impl District {
=                    .add_direction(&Direction::South)
=                    .add_direction(&Direction::North)
=            };
-            (Coordinates::new(&easternmost, &latitude), section)
+            (Coordinates::new(easternmost, *latitude), section)
=        });
=        let western_border = latitudinal_range.iter().map(|latitude| {
=            let section = if *latitude == northernmost {
@@ -106,7 +106,7 @@ impl District {
=                    .add_direction(&Direction::South)
=                    .add_direction(&Direction::North)
=            };
-            (Coordinates::new(&westernmost, &latitude), section)
+            (Coordinates::new(westernmost, *latitude), section)
=        });
=
=        let borders = northern_border
@@ -151,7 +151,7 @@ fn establish_new_districts(
=    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 origin = Coordinates::new(Longitude::from(x), Latitude::from(y));
=    let extent = origin
=        .clone()
=        .shift(18, Direction::East)