Week 12 of 2024
Development log of Otterhide
49 items
- Added single-big house, some more medium houses and small flat and big flat to buildings
- Remove dead, commented out code
- Run simulation in fixed frames per daymonth
- Create a simulation module with a progress bar
- The simulation advances when a button is pressed
- Fix simulation double advance on new months
- Fix time sometimes showing 60 minutes past hour
- Implement play / pause / step logic for simulation
- Make a variable immutable
- Remove some unnecasary type casting
- Elevate the sun for longer days
- Added supermarket, school placeholder. Minor model changes/fixes. Including another 3rd party model.
- Linked some new buildings to a new district.
- Added school to district that is being rendered.
- Rename the explore module + plugin to exploration
- Merge Settings, TimeSacele and date constants
- Use a slider to roll back and forward
- Add previous and next month buttons to explore
- Add playback speed control buttons
- Make playback buttons bigger
- Fix: no roads in the initial snapshot
- Changes to model sizes. Removed simple small house with a cube (kenney's model didn't make sense).
- Added cube for supermarket block.
- Create the people module with Savings component
- Establish new districts when needed by the people
- Construct buildings based on housing shortage.
- Immigrating 2 people per simulation frame.
- Immigrating people only at 8am
- Format the code
- Fix panic when housing shortage is negative
- Implement the daily run condition
- Rename daily run condition to past_hour
- Linked and added more varied buildings to districts (supermarket, school, library). Added Library. Resized supermarket and school.
- Give people names and sex
- Color code district models, correct sizing a bit
- Use all district models from .blend / .glb files
- Fix a bug where sometimes districts would overlap
- Rename: (establish -> plan)_new_districts
- Rename a parameter
- Merge Longitude and Latitude into Coordinate
- Refactor code in names module
- Test and prevent district_size from panicking
- Don't clone a date - it implements the Copy trait
- Let the check goal run tests and then clippy
- Improve building module naming convention
- Use wider number for buildings appearance
- Use a component bundle to spawn buildings
- Rename BuildingVariant appearance field to number
- Refactor districts module
Added single-big house, some more medium houses and small flat and big flat to buildings
On by
index 4394302..7006f1d 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differnew file mode 100644
index 0000000..69c3acd
Binary files /dev/null and b/art/models/building-sample-tower-a.glb differnew file mode 100644
index 0000000..b4d4c76
Binary files /dev/null and b/art/models/building-sample-tower-b.glb differnew file mode 100644
index 0000000..61d6e99
Binary files /dev/null and b/art/models/building-steps-wide.glb differnew file mode 100644
index 0000000..391f2e9
Binary files /dev/null and b/art/models/building-window-large.glb differnew file mode 100644
index 0000000..322e9a2
Binary files /dev/null and b/art/models/building-window-sill.glb differnew file mode 100644
index 0000000..92c7685
Binary files /dev/null and b/art/models/building-windows-high-top-square.glb differnew file mode 100644
index 0000000..366cb9b
Binary files /dev/null and b/art/models/roof-slanted.glb differindex 2cd2b93..c00110b 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differRemove dead, commented out code
On by
index ff80706..035d7c6 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -15,11 +15,7 @@ impl Plugin for CameraPlugin {
=
=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()
- })
+ .spawn(Camera3dBundle::default())
= .insert(PanOrbitCamera {
= alpha: Some(0.0),
= beta: Some(1.0),Run simulation in fixed frames per daymonth
On by
Previously the simulated duration of a frame depended on real-world duration, i.e. slower updates (e.g. due to performance regressions) would result in lower temporal resolution of the data set.
Now each frame advances the simulated time a fixed amount. By default it's ⅙ of a daymonth (i.e. 6 frames per daymonth). On slower systems the simulation is going to take longer, but will have same number of frames.
In exploration mode time is advanced the old way, i.e. correlated with real time.
Because of that, the old advance_date system is now split into two simpler ones - one for simulation, another for exploration.
index 36df5b5..a6615cc 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,5 +1,6 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
=use crate::{GameState, BEGINNING, DURATION};
+use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
@@ -12,16 +13,29 @@ impl Plugin for DatePlugin {
= app.insert_resource(Date(DayMonth::new(BEGINNING)))
= .register_type::<Timescale>()
= .init_resource::<Timescale>()
+ .init_resource::<FrameCounter>()
= .add_event::<NewMonth>()
= .add_systems(Startup, setup_date_display)
= .add_systems(Startup, register_date_systems)
- .add_systems(Update, (advance_date, update_date_display));
+ .add_systems(
+ PreUpdate,
+ advance_simulation_date.run_if(in_state(GameState::Simulate)),
+ )
+ .add_systems(
+ PreUpdate,
+ advance_exploration_date.run_if(in_state(GameState::Explore)),
+ )
+ .add_systems(PreUpdate, update_date_display);
= }
=}
=
-#[derive(Resource)]
+#[derive(Resource, Debug)]
=pub struct Date(pub DayMonth);
=
+/// Count the frames simulated so far
+#[derive(Resource, Default, Debug)]
+struct FrameCounter(u8);
+
=#[derive(Resource, Reflect, InspectorOptions)]
=#[reflect(Resource, InspectorOptions)]
=// TODO: It's hard to control small fractions. Make the scale logarithmic?
@@ -30,15 +44,17 @@ struct Timescale {
= #[inspector(min = 0.0, max = 2.0, display = NumberDisplay::Slider)]
= scale: f32,
= /// Day-months per one second of real time during simulation
- #[inspector(min = 0.0, max = 2.0, display = NumberDisplay::Slider)]
- simulation_scale: f32,
+ #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
+ simulation_fpd: u8,
+ simulation_frame_offset: f32,
=}
=
=impl Default for Timescale {
= fn default() -> Self {
= Self {
= scale: 1.0 / 10.0,
- simulation_scale: 2.0,
+ simulation_fpd: 6,
+ simulation_frame_offset: 2.0 * HOUR,
= }
= }
=}
@@ -46,35 +62,36 @@ impl Default for Timescale {
=#[derive(Event)]
=pub struct NewMonth;
=
-fn advance_date(
- time: Res<Time>,
+fn advance_simulation_date(
= mut date: ResMut<Date>,
- mut events: EventWriter<NewMonth>,
- game_state: Res<State<GameState>>,
+ mut new_month: EventWriter<NewMonth>,
= timescale: Res<Timescale>,
+ mut framecounter: ResMut<FrameCounter>,
=) {
+ framecounter.0 = u8::rem_euclid(framecounter.0 + 1, timescale.simulation_fpd);
+ if framecounter.0 == 0 {
+ date.0.advance_to_next();
+ info!("New month: {month}", month = date.0);
+ // IDEA: Maybe we can get rid of it and let all systems look into the frame counter?
+ new_month.send(NewMonth);
+ }
+
+ date.0.advance(1.0 / timescale.simulation_fpd as f32);
+}
+
+fn advance_exploration_date(time: Res<Time>, mut date: ResMut<Date>, timescale: Res<Timescale>) {
= // Do not go much over the duration, but gently slow down after
= const ENDTIME: f32 = 9.0 * HOUR + 32.0 * MINUTE;
- let mut scale = if game_state.get() == &GameState::Simulate {
- timescale.simulation_scale
+
+ let mut scale = if date.0.year() >= (BEGINNING + DURATION) {
+ timescale.scale * (ENDTIME - date.0.time_of_day()).max(0.0)
= } else {
= timescale.scale
= };
=
- if date.0.year() >= (BEGINNING + DURATION) {
- scale *= (ENDTIME - date.0.time_of_day()).max(0.0)
- };
-
- let month = date.0.month();
-
= let delta = time.delta_seconds();
= let duration = delta * scale;
= date.0.advance(duration);
-
- if date.0.month() != month {
- events.send(NewMonth);
- info!("It's a new month: {month}");
- }
=}
=
=// TODO: Consider if date display logic should go to a UI module?index cc8e080..1554d42 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -90,7 +90,7 @@ impl DayMonth {
= Self { year, month: 0.0 }
= }
=
- pub fn advance(&mut self, duration: f32) -> &Self {
+ pub fn advance(&mut self, duration: f32) -> &mut Self {
= let month = self.month + duration;
=
= self.month = month.rem_euclid(12.0);
@@ -98,6 +98,13 @@ impl DayMonth {
= self
= }
=
+ /// Advance to the beginning of the next month
+ pub fn advance_to_next(&mut self) -> &mut Self {
+ self.advance(1.0);
+ self.month = self.month.floor();
+ self
+ }
+
= pub fn year(&self) -> i32 {
= self.year
= }
@@ -135,6 +142,12 @@ impl Display for DayMonth {
= }
=}
=
+impl From<DayMonth> for f32 {
+ fn from(value: DayMonth) -> Self {
+ (value.year * 12) as f32 + value.month
+ }
+}
+
=#[cfg(test)]
=mod daymonth_tests {
= use super::*;Create a simulation module with a progress bar
On by
index a6615cc..8134f07 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,5 +1,5 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
-use crate::{GameState, BEGINNING, DURATION};
+use crate::simulation::{BEGINNING, DURATION};
=use crate::GameState;
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;index 88260a3..c9f6f37 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,6 +9,7 @@ mod ground;
=mod history;
=mod pgsql_export;
=mod roads;
+mod simulation;
=mod sun;
=
=use bevy::prelude::*;
@@ -25,11 +26,9 @@ use explore::ExplorePlugin;
=use ground::GroundPlugin;
=use history::HistoryPlugin;
=use roads::RoadsPlugin;
+use simulation::SimulationPlugin;
=use sun::SunPlugin;
=
-const BEGINNING: i32 = 1860;
-const DURATION: i32 = 4;
-
=fn main() {
= App::new()
= .add_plugins(DefaultPlugins.set(WindowPlugin {
@@ -56,6 +55,7 @@ fn main() {
= .add_plugins(SunPlugin)
= .add_plugins(HistoryPlugin)
= .add_plugins(WorldInspectorPlugin::new())
+ .add_plugins(SimulationPlugin)
= .add_plugins(ExplorePlugin)
= .add_systems(Startup, greet)
= .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
@@ -109,7 +109,7 @@ fn switch_states(
= return;
= }
=
- if date.0 > DayMonth::new(BEGINNING + DURATION) {
+ if date.0 > DayMonth::new(simulation::BEGINNING + simulation::DURATION) {
= state.set(GameState::Explore)
= }
=}new file mode 100644
index 0000000..8a934ce
--- /dev/null
+++ b/src/simulation.rs
@@ -0,0 +1,37 @@
+use crate::{date::Date, GameState};
+use bevy::{math::vec2, prelude::*};
+use bevy_egui::{
+ egui::{self, epaint::Shadow, Frame, Margin, ProgressBar, Stroke},
+ EguiContexts,
+};
+pub const BEGINNING: i32 = 1860;
+pub const DURATION: i32 = 4;
+
+pub struct SimulationPlugin;
+
+impl Plugin for SimulationPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(Update, paint_ui.run_if(in_state(GameState::Simulate)));
+ }
+}
+
+fn paint_ui(mut contexts: EguiContexts, date: Res<Date>, mut commands: Commands) {
+ let first_month = BEGINNING as f32 * 12.0;
+ let months_to_simulate = DURATION as f32 * 12.0;
+ let months_simulated = f32::from(date.0);
+ let progress = (months_simulated - first_month) / months_to_simulate;
+
+ info!("Progress: {progress} = ({months_simulated} - {first_month}) / {months_to_simulate}");
+
+ egui::TopBottomPanel::bottom("main")
+ .show_separator_line(false)
+ .frame(Frame {
+ inner_margin: Margin::symmetric(40., 20.),
+ shadow: Shadow::NONE,
+ stroke: Stroke::NONE,
+ ..default()
+ })
+ .show(contexts.ctx_mut(), |ui| {
+ ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
+ });
+}The simulation advances when a button is pressed
On by
Now next to the progress bar there is a step button that advances the simulation one frame.
index 8134f07..f8a9fb7 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -17,10 +17,6 @@ impl Plugin for DatePlugin {
= .add_event::<NewMonth>()
= .add_systems(Startup, setup_date_display)
= .add_systems(Startup, register_date_systems)
- .add_systems(
- PreUpdate,
- advance_simulation_date.run_if(in_state(GameState::Simulate)),
- )
= .add_systems(
= PreUpdate,
= advance_exploration_date.run_if(in_state(GameState::Explore)),
@@ -139,14 +135,17 @@ fn rollback(In(snapshot): In<Snapshot>, mut date: ResMut<Date>) {
=pub struct DateSystems {
= pub take_snapshot: SystemId<(), Snapshot>,
= pub rollback: SystemId<Snapshot>,
+ pub advance_simulation_date: SystemId,
=}
=
=fn register_date_systems(world: &mut World) {
= let take_snapshot = world.register_system(take_snapshot);
= let rollback = world.register_system(rollback);
+ let advance_simulation_date = world.register_system(advance_simulation_date);
=
= world.insert_resource(DateSystems {
= take_snapshot,
= rollback,
+ advance_simulation_date,
= });
=}index 8a934ce..d363fa3 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -1,9 +1,11 @@
-use crate::{date::Date, GameState};
-use bevy::{math::vec2, prelude::*};
+use crate::date::{Date, DateSystems};
+use crate::GameState;
+use bevy::prelude::*;
=use bevy_egui::{
= egui::{self, epaint::Shadow, Frame, Margin, ProgressBar, Stroke},
= EguiContexts,
=};
+
=pub const BEGINNING: i32 = 1860;
=pub const DURATION: i32 = 4;
=
@@ -15,7 +17,12 @@ impl Plugin for SimulationPlugin {
= }
=}
=
-fn paint_ui(mut contexts: EguiContexts, date: Res<Date>, mut commands: Commands) {
+fn paint_ui(
+ mut contexts: EguiContexts,
+ date: Res<Date>,
+ mut commands: Commands,
+ systems: Res<DateSystems>,
+) {
= let first_month = BEGINNING as f32 * 12.0;
= let months_to_simulate = DURATION as f32 * 12.0;
= let months_simulated = f32::from(date.0);
@@ -32,6 +39,14 @@ fn paint_ui(mut contexts: EguiContexts, date: Res<Date>, mut commands: Commands)
= ..default()
= })
= .show(contexts.ctx_mut(), |ui| {
- ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
+ let layout = egui::Layout::left_to_right(egui::Align::Center);
+ ui.with_layout(layout, |ui| {
+ ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
+ let step_button = ui.button(">|"); // TODO: Use a nice icon
+ if step_button.clicked() {
+ commands.run_system(systems.advance_simulation_date);
+ }
+ ui.end_row();
+ })
= });
=}Fix simulation double advance on new months
On by
Also actually take the offset setting into consideration.
index f8a9fb7..92ea547 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -10,9 +10,13 @@ pub struct DatePlugin;
=
=impl Plugin for DatePlugin {
= fn build(&self, app: &mut App) {
- app.insert_resource(Date(DayMonth::new(BEGINNING)))
+ let timescale = Timescale::default();
+ let date = Date(*DayMonth::new(BEGINNING).advance(timescale.simulation_frame_offset));
+
+ app.insert_resource(date)
= .register_type::<Timescale>()
- .init_resource::<Timescale>()
+ .insert_resource::<Timescale>(timescale)
+ .register_type::<FrameCounter>()
= .init_resource::<FrameCounter>()
= .add_event::<NewMonth>()
= .add_systems(Startup, setup_date_display)
@@ -29,7 +33,8 @@ impl Plugin for DatePlugin {
=pub struct Date(pub DayMonth);
=
=/// Count the frames simulated so far
-#[derive(Resource, Default, Debug)]
+#[derive(Resource, Default, Debug, Reflect)]
+#[reflect(Resource)]
=struct FrameCounter(u8);
=
=#[derive(Resource, Reflect, InspectorOptions)]
@@ -42,6 +47,9 @@ struct Timescale {
= /// Day-months per one second of real time during simulation
= #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
= simulation_fpd: u8,
+ /// What time the first frame of the day starts
+ ///
+ /// 0.0 - midnight. 0.5 - noon
= simulation_frame_offset: f32,
=}
=
@@ -66,13 +74,15 @@ fn advance_simulation_date(
=) {
= framecounter.0 = u8::rem_euclid(framecounter.0 + 1, timescale.simulation_fpd);
= if framecounter.0 == 0 {
- date.0.advance_to_next();
+ date.0
+ .advance_to_next()
+ .advance(timescale.simulation_frame_offset);
= info!("New month: {month}", month = date.0);
= // IDEA: Maybe we can get rid of it and let all systems look into the frame counter?
= new_month.send(NewMonth);
+ } else {
+ date.0.advance(1.0 / timescale.simulation_fpd as f32);
= }
-
- date.0.advance(1.0 / timescale.simulation_fpd as f32);
=}
=
=fn advance_exploration_date(time: Res<Time>, mut date: ResMut<Date>, timescale: Res<Timescale>) {Fix time sometimes showing 60 minutes past hour
On by
index 1554d42..1a8be90 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -113,13 +113,17 @@ impl DayMonth {
= Month::new(self.month as i32 + 1)
= }
=
+ // A private helper for hour and minute
+ fn minutes_i32(&self) -> i32 {
+ (self.month.rem_euclid(1.0) * 60.0 * 24.0) as i32
+ }
+
= pub fn hour(&self) -> i32 {
- (self.month.rem_euclid(1.0) * 24.0) as i32
+ self.minutes_i32().div_euclid(60)
= }
=
- pub fn minute(&self) -> f32 {
- // TODO: Can minutes calculation be simplified for efficiency?
- (self.month.rem_euclid(1.0) * 24.0).rem_euclid(1.0) * 60.0
+ pub fn minute(&self) -> i32 {
+ self.minutes_i32().rem_euclid(60)
= }
=
= /// Return a number between 0.0 and 1.0 representing the fraction of the day that passed
@@ -137,7 +141,7 @@ impl Display for DayMonth {
= self.month(),
= self.year(),
= self.hour(),
- self.minute()
+ self.minute(),
= )
= }
=}
@@ -159,7 +163,7 @@ mod daymonth_tests {
= assert_eq!(daymonth.year(), 1972);
= assert_eq!(daymonth.month(), Month::January);
= assert_eq!(daymonth.hour(), 0);
- assert_eq!(daymonth.minute(), 0.0);
+ assert_eq!(daymonth.minute(), 0);
= }
=
= #[test]Implement play / pause / step logic for simulation
On by
A control-click steps one frame.
index d363fa3..7efdcdb 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -13,13 +13,30 @@ pub struct SimulationPlugin;
=
=impl Plugin for SimulationPlugin {
= fn build(&self, app: &mut App) {
- app.add_systems(Update, paint_ui.run_if(in_state(GameState::Simulate)));
+ app.register_type::<AutoAdvance>()
+ .init_resource::<AutoAdvance>()
+ .add_systems(
+ PreUpdate,
+ auto_advance.run_if(
+ resource_equals(AutoAdvance(true)).and_then(in_state(GameState::Simulate)),
+ ),
+ )
+ .add_systems(Update, paint_ui.run_if(in_state(GameState::Simulate)));
= }
=}
=
+#[derive(Resource, Debug, Default, Reflect, PartialEq, Eq)]
+#[reflect(Resource)]
+pub struct AutoAdvance(bool);
+
+fn auto_advance(mut commands: Commands, systems: Res<DateSystems>) {
+ commands.run_system(systems.advance_simulation_date);
+}
+
=fn paint_ui(
= mut contexts: EguiContexts,
= date: Res<Date>,
+ mut auto_advance: ResMut<AutoAdvance>,
= mut commands: Commands,
= systems: Res<DateSystems>,
=) {
@@ -28,8 +45,6 @@ fn paint_ui(
= let months_simulated = f32::from(date.0);
= let progress = (months_simulated - first_month) / months_to_simulate;
=
- info!("Progress: {progress} = ({months_simulated} - {first_month}) / {months_to_simulate}");
-
= egui::TopBottomPanel::bottom("main")
= .show_separator_line(false)
= .frame(Frame {
@@ -42,9 +57,22 @@ fn paint_ui(
= let layout = egui::Layout::left_to_right(egui::Align::Center);
= ui.with_layout(layout, |ui| {
= ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
- let step_button = ui.button(">|"); // TODO: Use a nice icon
- if step_button.clicked() {
- commands.run_system(systems.advance_simulation_date);
+
+ if auto_advance.0 {
+ let pause_button = ui.button("⏸");
+ if pause_button.clicked() {
+ auto_advance.0 = false;
+ }
+ } else if ui.input(|i| i.modifiers.ctrl) {
+ let step_button = ui.button("⏯"); // FIXME: Missing glyph
+ if step_button.clicked() {
+ commands.run_system(systems.advance_simulation_date);
+ }
+ } else {
+ let play_button = ui.button("▶");
+ if play_button.clicked() {
+ auto_advance.0 = true;
+ };
= }
= ui.end_row();
= })Make a variable immutable
On by
index 92ea547..a9e8f1a 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -89,7 +89,7 @@ fn advance_exploration_date(time: Res<Time>, mut date: ResMut<Date>, timescale:
= // Do not go much over the duration, but gently slow down after
= const ENDTIME: f32 = 9.0 * HOUR + 32.0 * MINUTE;
=
- let mut scale = if date.0.year() >= (BEGINNING + DURATION) {
+ let scale = if date.0.year() >= (BEGINNING + DURATION) {
= timescale.scale * (ENDTIME - date.0.time_of_day()).max(0.0)
= } else {
= timescale.scaleRemove some unnecasary type casting
On by
index fd086e8..372579a 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -238,7 +238,7 @@ impl From<DayMonth> for Timestamp {
= year: day_month.year(),
= month: day_month.month(),
= hour: day_month.hour(),
- minute: day_month.minute() as i32,
+ minute: day_month.minute(),
= }
= }
=}index 7bfe650..c705676 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -28,7 +28,7 @@ impl Display for History {
= let month = date.month() as i32;
= let day = 01;
= let hour = date.hour();
- let minute = date.minute() as i32;
+ let minute = date.minute();
= let second = 00;
=
= match event.event.clone() {Elevate the sun for longer days
On by
Now the sun orbits a point 600m above the city, so at 06:00 and 18:00 it's still above the horizon.
index c9f6f37..9294638 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -42,7 +42,8 @@ fn main() {
= }))
= .insert_resource(Settings {
= land_radius: 2000.,
- sun_gap: 100.,
+ sun_gap: 200.,
+ sun_elevation: 600.,
= })
= .init_resource::<PreloadedAssets>()
= .init_state::<GameState>()
@@ -66,8 +67,12 @@ fn main() {
=
=#[derive(Debug, Resource)]
=pub struct Settings {
+ /// How big is the landmass on which Otterhide is built
= land_radius: f32,
+ /// Extra distance from the edge of landmass to the sun
= sun_gap: f32,
+ /// Elevate the canter of sun's orbit for longer days
+ sun_elevation: f32,
=}
=
=fn greet() {index 02a2168..422dc7b 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -40,15 +40,17 @@ fn setup_sunlight(mut commands: Commands, settings: Res<Settings>) {
=}
=
=fn move_sun(mut sun: Query<&mut Transform, With<Sun>>, date: Res<Date>, settings: Res<Settings>) {
+ // TODO: Take elevation into account, so that the gap is actually between the endge of land and the sun
= let orbit = settings.land_radius + settings.sun_gap;
=
= let daytime = date.0.time_of_day();
= let angle = daytime * 2.0 * PI - PI;
+ // TODO: Lean the orbit more in winters.
= let rotation = Quat::from_rotation_x(angle) * Quat::from_rotation_z(1.0);
= let translation = rotation.mul_vec3(Vec3::Y) * orbit;
=
= let mut transform = sun.single_mut();
- transform.translation = translation;
+ transform.translation = translation + Vec3::Y * settings.sun_elevation;
= transform.look_at(Vec3::ZERO, Vec3::Y);
=}
=Added supermarket, school placeholder. Minor model changes/fixes. Including another 3rd party model.
On by
index 7006f1d..557db65 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differnew file mode 100644
index 0000000..e6100e6
Binary files /dev/null and b/art/models/pitsGarageCorner.glb differLinked some new buildings to a new district.
On by
index 557db65..bb14d7d 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differindex f21b365..d4d5ef4 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex c00110b..7b5ac59 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differindex 6954a33..f98fa47 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differAdded school to district that is being rendered.
On by
index bb14d7d..76d6e62 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differindex d4d5ef4..d3320fe 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 7b5ac59..2ffa7ca 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differindex f98fa47..b03ac12 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differRename the explore module + plugin to exploration
On by
Sounds more right :P
similarity index 95%
rename from src/explore.rs
rename to src/exploration.rs
index 6499b6f..3236154 100644
--- a/src/explore.rs
+++ b/src/exploration.rs
@@ -7,9 +7,9 @@ use bevy_egui::egui;
=use bevy_egui::EguiContexts;
=use itertools::Itertools;
=
-pub struct ExplorePlugin;
+pub struct ExplorationPlugin;
=
-impl Plugin for ExplorePlugin {
+impl Plugin for ExplorationPlugin {
= fn build(&self, app: &mut App) {
= app.add_systems(Update, paint_ui.run_if(in_state(GameState::Explore)));
= }index 9294638..fa94f9b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,7 +4,7 @@ mod coordinates;
=mod date;
=mod day_month;
=mod districts;
-mod explore;
+mod exploration;
=mod ground;
=mod history;
=mod pgsql_export;
@@ -22,7 +22,7 @@ use date::DatePlugin;
=use date::NewMonth;
=use day_month::DayMonth;
=use districts::DistrictsPlugin;
-use explore::ExplorePlugin;
+use exploration::ExplorationPlugin;
=use ground::GroundPlugin;
=use history::HistoryPlugin;
=use roads::RoadsPlugin;
@@ -57,7 +57,7 @@ fn main() {
= .add_plugins(HistoryPlugin)
= .add_plugins(WorldInspectorPlugin::new())
= .add_plugins(SimulationPlugin)
- .add_plugins(ExplorePlugin)
+ .add_plugins(ExplorationPlugin)
= .add_systems(Startup, greet)
= .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
= .add_systems(Update, switch_states)Merge Settings, TimeSacele and date constants
On by
...into SimulationParameters resource. On the way I cleaned up a lot of messy code into less messy code.
index a9e8f1a..ebe1a33 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -1,6 +1,5 @@
=use crate::day_month::{DayMonth, HOUR, MINUTE};
-use crate::simulation::{BEGINNING, DURATION};
-use crate::GameState;
+use crate::{GameState, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
@@ -10,12 +9,12 @@ pub struct DatePlugin;
=
=impl Plugin for DatePlugin {
= fn build(&self, app: &mut App) {
- let timescale = Timescale::default();
- let date = Date(*DayMonth::new(BEGINNING).advance(timescale.simulation_frame_offset));
+ let simulation = app.world.resource::<SimulationParameters>();
+ let date = simulation.beginning();
=
- app.insert_resource(date)
+ app.insert_resource(Date(date))
= .register_type::<Timescale>()
- .insert_resource::<Timescale>(timescale)
+ .init_resource::<Timescale>()
= .register_type::<FrameCounter>()
= .init_resource::<FrameCounter>()
= .add_event::<NewMonth>()
@@ -32,6 +31,15 @@ impl Plugin for DatePlugin {
=#[derive(Resource, Debug)]
=pub struct Date(pub DayMonth);
=
+impl Date {
+ pub fn fraction_of_simulation(&self, simulation: &SimulationParameters) -> f32 {
+ let first_month = (simulation.first_year * 12) as f32;
+ let months_to_simulate = (simulation.years * 12) as f32;
+ let months_simulated = f32::from(&self.0);
+ (months_simulated - first_month) / months_to_simulate
+ }
+}
+
=/// Count the frames simulated so far
=#[derive(Resource, Default, Debug, Reflect)]
=#[reflect(Resource)]
@@ -44,22 +52,11 @@ struct Timescale {
= /// Day-months per one second of real time during exploration
= #[inspector(min = 0.0, max = 2.0, display = NumberDisplay::Slider)]
= scale: f32,
- /// Day-months per one second of real time during simulation
- #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
- simulation_fpd: u8,
- /// What time the first frame of the day starts
- ///
- /// 0.0 - midnight. 0.5 - noon
- simulation_frame_offset: f32,
=}
=
=impl Default for Timescale {
= fn default() -> Self {
- Self {
- scale: 1.0 / 10.0,
- simulation_fpd: 6,
- simulation_frame_offset: 2.0 * HOUR,
- }
+ Self { scale: 1.0 / 10.0 }
= }
=}
=
@@ -69,27 +66,30 @@ pub struct NewMonth;
=fn advance_simulation_date(
= mut date: ResMut<Date>,
= mut new_month: EventWriter<NewMonth>,
- timescale: Res<Timescale>,
= mut framecounter: ResMut<FrameCounter>,
+ simulation: Res<SimulationParameters>,
=) {
- framecounter.0 = u8::rem_euclid(framecounter.0 + 1, timescale.simulation_fpd);
+ framecounter.0 = u8::rem_euclid(framecounter.0 + 1, simulation.frames_per_day);
= if framecounter.0 == 0 {
- date.0
- .advance_to_next()
- .advance(timescale.simulation_frame_offset);
+ date.0.advance_to_next().advance(simulation.frame_offset);
= info!("New month: {month}", month = date.0);
= // IDEA: Maybe we can get rid of it and let all systems look into the frame counter?
= new_month.send(NewMonth);
= } else {
- date.0.advance(1.0 / timescale.simulation_fpd as f32);
+ date.0.advance(1.0 / simulation.frames_per_day as f32);
= }
=}
=
-fn advance_exploration_date(time: Res<Time>, mut date: ResMut<Date>, timescale: Res<Timescale>) {
+fn advance_exploration_date(
+ time: Res<Time>,
+ mut date: ResMut<Date>,
+ timescale: Res<Timescale>,
+ simulation: Res<SimulationParameters>,
+) {
= // Do not go much over the duration, but gently slow down after
= const ENDTIME: f32 = 9.0 * HOUR + 32.0 * MINUTE;
=
- let scale = if date.0.year() >= (BEGINNING + DURATION) {
+ let scale = if date.0 > simulation.end() {
= timescale.scale * (ENDTIME - date.0.time_of_day()).max(0.0)
= } else {
= timescale.scaleindex 1a8be90..3c94f51 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -146,8 +146,8 @@ impl Display for DayMonth {
= }
=}
=
-impl From<DayMonth> for f32 {
- fn from(value: DayMonth) -> Self {
+impl From<&DayMonth> for f32 {
+ fn from(value: &DayMonth) -> Self {
= (value.year * 12) as f32 + value.month
= }
=}index 6526a06..ab5eaf6 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -2,7 +2,7 @@ use crate::buildings::building_class;
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
=use crate::date::NewMonth;
=use crate::roads::{RoadPlan, RoadsSystems};
-use crate::{GameState, PreloadedAssets, Settings};
+use crate::{GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -297,7 +297,7 @@ impl From<&District> for Rect {
=
=fn establish_new_districts(
= districts: Query<&District>,
- settings: Res<Settings>,
+ settings: Res<SimulationParameters>,
= mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
= // TODO: Read from the asset (scene)index aad64a3..f75a053 100644
--- a/src/ground.rs
+++ b/src/ground.rs
@@ -1,4 +1,4 @@
-use crate::Settings;
+use crate::SimulationParameters;
=use bevy::prelude::*;
=use std::f32::consts::FRAC_PI_2;
=
@@ -17,7 +17,7 @@ fn setup_ground(
= mut commands: Commands,
= mut meshes: ResMut<Assets<Mesh>>,
= mut materials: ResMut<Assets<StandardMaterial>>,
- settings: Res<Settings>,
+ settings: Res<SimulationParameters>,
=) {
= commands
= .spawn(PbrBundle {index fa94f9b..c12076f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,6 +14,8 @@ mod sun;
=
=use bevy::prelude::*;
=use bevy::utils::HashSet;
+use bevy_inspector_egui::inspector_options::std_options::NumberDisplay;
+use bevy_inspector_egui::prelude::*;
=use bevy_inspector_egui::quick::WorldInspectorPlugin;
=use buildings::BuildingsPlugin;
=use camera::CameraPlugin;
@@ -21,6 +23,7 @@ use date::Date;
=use date::DatePlugin;
=use date::NewMonth;
=use day_month::DayMonth;
+use day_month::HOUR;
=use districts::DistrictsPlugin;
=use exploration::ExplorationPlugin;
=use ground::GroundPlugin;
@@ -40,11 +43,8 @@ fn main() {
= }),
= ..default()
= }))
- .insert_resource(Settings {
- land_radius: 2000.,
- sun_gap: 200.,
- sun_elevation: 600.,
- })
+ .register_type::<SimulationParameters>()
+ .init_resource::<SimulationParameters>()
= .init_resource::<PreloadedAssets>()
= .init_state::<GameState>()
= .add_plugins(CameraPlugin)
@@ -60,19 +60,59 @@ fn main() {
= .add_plugins(ExplorationPlugin)
= .add_systems(Startup, greet)
= .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
- .add_systems(Update, switch_states)
+ .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()))
= .add_systems(OnEnter(GameState::Explore), setup_exploration)
= .run()
=}
=
-#[derive(Debug, Resource)]
-pub struct Settings {
+/// Immutable parameters of a simulation.
+///
+/// Changing them in the middle of a running simulation will have unspecified
+/// and probably horrible effects. If this parameters are changed, the
+/// simulation has to be re-run.
+#[derive(Debug, Resource, Reflect, InspectorOptions)]
+#[reflect(Resource)]
+pub struct SimulationParameters {
= /// How big is the landmass on which Otterhide is built
- land_radius: f32,
+ pub land_radius: f32,
= /// Extra distance from the edge of landmass to the sun
- sun_gap: f32,
+ pub sun_gap: f32,
= /// Elevate the canter of sun's orbit for longer days
- sun_elevation: f32,
+ pub sun_elevation: f32,
+ // IDEA: We can simplify some calculations by using this value only for Display
+ pub first_year: i32,
+ // The length of the simulation
+ pub years: u16,
+ /// Day-months per one second of real time during simulation
+ #[inspector(min = 2, max = 255, display = NumberDisplay::Slider)]
+ pub frames_per_day: u8,
+ /// What time the first frame of the day starts
+ ///
+ /// 0.0 - midnight. 0.5 - noon
+ pub frame_offset: f32,
+}
+
+impl Default for SimulationParameters {
+ fn default() -> Self {
+ Self {
+ land_radius: 2000.,
+ sun_gap: 200.,
+ sun_elevation: 600.,
+ first_year: 1865,
+ years: 2,
+ frame_offset: 2.0 * HOUR,
+ frames_per_day: 6,
+ }
+ }
+}
+
+impl SimulationParameters {
+ pub fn beginning(&self) -> DayMonth {
+ *DayMonth::new(self.first_year).advance(self.frame_offset)
+ }
+ pub fn end(&self) -> DayMonth {
+ *DayMonth::new(self.first_year + self.years as i32).advance(self.frame_offset)
+ }
=}
=
=fn greet() {
@@ -106,15 +146,11 @@ fn preload_assets(
=}
=
=fn switch_states(
- mut new_month_events: EventReader<NewMonth>,
= date: Res<Date>,
= mut state: ResMut<NextState<GameState>>,
+ simulation: Res<SimulationParameters>,
=) {
- if new_month_events.read().count() == 0 {
- return;
- }
-
- if date.0 > DayMonth::new(simulation::BEGINNING + simulation::DURATION) {
+ if date.0 > simulation.end() {
= state.set(GameState::Explore)
= }
=}index 0196fb4..620f17b 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,5 +1,5 @@
=use crate::coordinates::{Coordinates, Direction};
-use crate::{GameState, PreloadedAssets, Settings};
+use crate::{GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -38,7 +38,7 @@ fn setup_assets(
=
=fn lay_initial_roads(
= mut commands: Commands,
- settings: Res<Settings>,
+ settings: Res<SimulationParameters>,
= constructors: Res<RoadsSystems>,
=) {
= info!("Laying initial roads.");index 7efdcdb..eac5122 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -1,14 +1,11 @@
=use crate::date::{Date, DateSystems};
-use crate::GameState;
+use crate::{GameState, SimulationParameters};
=use bevy::prelude::*;
=use bevy_egui::{
= egui::{self, epaint::Shadow, Frame, Margin, ProgressBar, Stroke},
= EguiContexts,
=};
=
-pub const BEGINNING: i32 = 1860;
-pub const DURATION: i32 = 4;
-
=pub struct SimulationPlugin;
=
=impl Plugin for SimulationPlugin {
@@ -38,13 +35,9 @@ fn paint_ui(
= date: Res<Date>,
= mut auto_advance: ResMut<AutoAdvance>,
= mut commands: Commands,
+ simulation: Res<SimulationParameters>,
= systems: Res<DateSystems>,
=) {
- let first_month = BEGINNING as f32 * 12.0;
- let months_to_simulate = DURATION as f32 * 12.0;
- let months_simulated = f32::from(date.0);
- let progress = (months_simulated - first_month) / months_to_simulate;
-
= egui::TopBottomPanel::bottom("main")
= .show_separator_line(false)
= .frame(Frame {
@@ -56,6 +49,7 @@ fn paint_ui(
= .show(contexts.ctx_mut(), |ui| {
= let layout = egui::Layout::left_to_right(egui::Align::Center);
= ui.with_layout(layout, |ui| {
+ let progress = date.fraction_of_simulation(&simulation);
= ui.add(ProgressBar::new(progress).text(date.0.year().to_string()));
=
= if auto_advance.0 {index 422dc7b..71db7d9 100644
--- a/src/sun.rs
+++ b/src/sun.rs
@@ -1,5 +1,5 @@
=use crate::date::Date;
-use crate::Settings;
+use crate::SimulationParameters;
=use bevy::prelude::*;
=use std::f32::consts::PI;
=use std::ops::{Mul, Sub};
@@ -22,7 +22,7 @@ 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, settings: Res<Settings>) {
+fn setup_sunlight(mut commands: Commands, settings: Res<SimulationParameters>) {
= let orbit = settings.land_radius + settings.sun_gap;
=
= commands
@@ -39,7 +39,11 @@ fn setup_sunlight(mut commands: Commands, settings: Res<Settings>) {
= .insert(Sun);
=}
=
-fn move_sun(mut sun: Query<&mut Transform, With<Sun>>, date: Res<Date>, settings: Res<Settings>) {
+fn move_sun(
+ mut sun: Query<&mut Transform, With<Sun>>,
+ date: Res<Date>,
+ settings: Res<SimulationParameters>,
+) {
= // TODO: Take elevation into account, so that the gap is actually between the endge of land and the sun
= let orbit = settings.land_radius + settings.sun_gap;
=Use a slider to roll back and forward
On by
Stylized as a video position indicator.
index ebe1a33..6199782 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -33,10 +33,13 @@ pub struct Date(pub DayMonth);
=
=impl Date {
= pub fn fraction_of_simulation(&self, simulation: &SimulationParameters) -> f32 {
- let first_month = (simulation.first_year * 12) as f32;
= let months_to_simulate = (simulation.years * 12) as f32;
- let months_simulated = f32::from(&self.0);
- (months_simulated - first_month) / months_to_simulate
+ self.months_simulated(simulation) / months_to_simulate
+ }
+ pub fn months_simulated(&self, simulation: &SimulationParameters) -> f32 {
+ let first_month = (simulation.first_year * 12) as f32;
+ let current_month = f32::from(&self.0);
+ current_month - first_month
= }
=}
=index 3236154..7aea0f7 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -1,51 +1,113 @@
+use crate::date::Date;
=use crate::history::History;
=use crate::history::HistorySystems;
=use crate::pgsql_export;
=use crate::GameState;
+use crate::SimulationParameters;
=use bevy::prelude::*;
=use bevy_egui::egui;
+use bevy_egui::egui::epaint::Shadow;
+use bevy_egui::egui::Frame;
+use bevy_egui::egui::Margin;
+use bevy_egui::egui::Slider;
+use bevy_egui::egui::Stroke;
=use bevy_egui::EguiContexts;
-use itertools::Itertools;
=
=pub struct ExplorationPlugin;
=
=impl Plugin for ExplorationPlugin {
= fn build(&self, app: &mut App) {
- app.add_systems(Update, paint_ui.run_if(in_state(GameState::Explore)));
+ app.register_type::<DateSliderValue>()
+ .init_resource::<DateSliderValue>()
+ .add_systems(
+ PostUpdate,
+ (
+ update_slider_value,
+ paint_ui,
+ apply_slider_value.run_if(resource_changed::<DateSliderValue>),
+ )
+ .chain()
+ .run_if(in_state(GameState::Explore)),
+ );
= }
=}
=
+#[derive(Resource, Reflect, Debug, Default)]
+#[reflect(Resource)]
+struct DateSliderValue(f64);
+
=fn paint_ui(
= mut contexts: EguiContexts,
= history: Res<History>,
+ mut slider_value: ResMut<DateSliderValue>,
+) {
+ egui::TopBottomPanel::bottom("lower_panel")
+ .show_separator_line(false)
+ .frame(Frame {
+ inner_margin: Margin::symmetric(40., 20.),
+ shadow: Shadow::NONE,
+ stroke: Stroke::NONE,
+ ..default()
+ })
+ .show(contexts.ctx_mut(), |ui| {
+ let size = ui.available_size();
+ ui.spacing_mut().slider_width = size.x;
+ egui::Grid::new("playback_controls_grid").show(ui, |ui| {
+ ui.horizontal(|ui| {
+ let export_button = ui.button("⏏");
+ if export_button.clicked() {
+ let exported = pgsql_export::export(history.as_ref());
+ // TODO: Save a file
+ info!("Export:\n\n{exported}");
+ }
+ });
+
+ ui.end_row();
+
+ let snapshots_count = (history.snapshots.len() - 1) as f64;
+ let range = 0.0..=snapshots_count;
+
+ let slider = Slider::from_get_set(range, |input| {
+ if let Some(value) = input {
+ slider_value.0 = value;
+ value
+ } else {
+ slider_value.0
+ }
+ })
+ .show_value(false)
+ .trailing_fill(true)
+ .handle_shape(egui::style::HandleShape::Rect { aspect_ratio: 0.5 })
+ // NOTE: There is an implicit assumption here that snapshots are taken every month
+ .integer();
+ ui.add(slider);
+ })
+ });
+}
+
+fn update_slider_value(
+ mut slider_value: ResMut<DateSliderValue>,
+ date: Res<Date>,
+ simulation: Res<SimulationParameters>,
+) {
+ slider_value.bypass_change_detection().0 = date.months_simulated(&simulation) as f64;
+}
+
+fn apply_slider_value(
+ slider_value: ResMut<DateSliderValue>,
+ history: Res<History>,
= systems: Res<HistorySystems>,
= mut commands: Commands,
=) {
- egui::SidePanel::right("explore_ui").show(contexts.ctx_mut(), |ui| {
- ui.heading("Explore");
-
- let export_button = ui.button("Export");
- if export_button.clicked() {
- let exported = pgsql_export::export(history.as_ref());
- // TODO: Save a file
- info!("Export:\n\n{exported}");
- }
-
- egui::ScrollArea::vertical().show(ui, |ui| {
- for (timestamp, snapshot) in history
- .snapshots
- .iter()
- // TODO: Consider using BTreeMap for always sorted snapshots. Or even ditch keys all together and use a vector?
- .sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
- {
- let month = timestamp.month;
- let year = timestamp.year;
- let button = ui.button(format!("{month} {year}"));
- if button.clicked() {
- info!("Time travel to {timestamp}");
- commands.run_system_with_input(systems.rollback, snapshot.clone())
- }
- }
- })
- });
+ let index = slider_value.0 as usize;
+ info!(
+ "Loading snapshot {index} out of {count}",
+ count = history.snapshots.len()
+ );
+ let Some(snapshot) = history.snapshots.get(index) else {
+ warn!("No snapshot at index {index}!");
+ return;
+ };
+
+ commands.run_system_with_input(systems.rollback, snapshot.clone());
=}index 372579a..b74cbd9 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -7,7 +7,6 @@ use crate::roads::{self, RoadsSystems};
=use crate::{districts, GameState};
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
-use bevy::utils::HashMap;
=use std::fmt::Display;
=use std::ops::DerefMut;
=
@@ -18,6 +17,7 @@ impl Plugin for HistoryPlugin {
= app.init_resource::<History>()
= .init_resource::<Future>()
= .add_systems(Startup, setup_history_systems)
+ .add_systems(OnEnter(GameState::Simulate), take_snapshot)
= .add_systems(PreUpdate, take_snapshot.run_if(on_event::<NewMonth>()))
= .add_systems(
= Update,
@@ -33,7 +33,7 @@ impl Plugin for HistoryPlugin {
=#[derive(Resource, Default, Debug)]
=pub struct History {
= pub events: Vec<EventLogEntry>,
- pub snapshots: HashMap<Timestamp, Snapshot>,
+ pub snapshots: Vec<Snapshot>,
=}
=
=/// A collection of events that will happen in the future from the time traveler's perspective
@@ -144,7 +144,7 @@ fn take_snapshot(world: &mut World) {
= };
=
= world.resource_scope(|_, mut history: Mut<History>| {
- history.deref_mut().snapshots.insert(timestamp, snapshot);
+ history.deref_mut().snapshots.push(snapshot);
= let count = history.snapshots.len();
= info!("Registering snapshot {count} on {timestamp}.")
= })index eac5122..e735fb0 100644
--- a/src/simulation.rs
+++ b/src/simulation.rs
@@ -68,7 +68,6 @@ fn paint_ui(
= auto_advance.0 = true;
= };
= }
- ui.end_row();
= })
= });
=}Add previous and next month buttons to explore
On by
index 7aea0f7..8fbbf03 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -54,6 +54,14 @@ fn paint_ui(
= ui.spacing_mut().slider_width = size.x;
= egui::Grid::new("playback_controls_grid").show(ui, |ui| {
= ui.horizontal(|ui| {
+ let previous_button = ui.button("⏮");
+ if previous_button.clicked() {
+ slider_value.0 = (slider_value.0 - 0.3).floor();
+ }
+ let next_button = ui.button("⏭");
+ if next_button.clicked() {
+ slider_value.0 = (slider_value.0 + 0.3).ceil();
+ }
= let export_button = ui.button("⏏");
= if export_button.clicked() {
= let exported = pgsql_export::export(history.as_ref());Add playback speed control buttons
On by
Pause, play, fast-forward.
index 6199782..a359854 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -59,7 +59,7 @@ struct Timescale {
=
=impl Default for Timescale {
= fn default() -> Self {
- Self { scale: 1.0 / 10.0 }
+ Self { scale: 1.0 / 120.0 }
= }
=}
=index 8fbbf03..ca33f56 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -40,6 +40,7 @@ fn paint_ui(
= mut contexts: EguiContexts,
= history: Res<History>,
= mut slider_value: ResMut<DateSliderValue>,
+ mut time: ResMut<Time<Virtual>>,
=) {
= egui::TopBottomPanel::bottom("lower_panel")
= .show_separator_line(false)
@@ -58,10 +59,33 @@ fn paint_ui(
= if previous_button.clicked() {
= slider_value.0 = (slider_value.0 - 0.3).floor();
= }
+
+ if time.is_paused() || time.relative_speed() != 1.0 {
+ let play_button = ui.button("▶");
+ if play_button.clicked() {
+ time.unpause();
+ time.set_relative_speed(1.0);
+ };
+ } else {
+ let pause_button = ui.button("⏸");
+ if pause_button.clicked() {
+ time.pause();
+ }
+ }
+
+ let next_speed = (time.relative_speed() * 2.0).min(128.0);
+ let label = format!("⏩ ×{next_speed:.0}");
+ let export_button = ui.button(label);
+ if export_button.clicked() {
+ time.unpause();
+ time.set_relative_speed(next_speed);
+ }
+
= let next_button = ui.button("⏭");
= if next_button.clicked() {
= slider_value.0 = (slider_value.0 + 0.3).ceil();
= }
+
= let export_button = ui.button("⏏");
= if export_button.clicked() {
= let exported = pgsql_export::export(history.as_ref());Make playback buttons bigger
On by
index ca33f56..38711a5 100644
--- a/src/exploration.rs
+++ b/src/exploration.rs
@@ -55,19 +55,21 @@ fn paint_ui(
= ui.spacing_mut().slider_width = size.x;
= egui::Grid::new("playback_controls_grid").show(ui, |ui| {
= ui.horizontal(|ui| {
- let previous_button = ui.button("⏮");
+ let min_button_size = egui::Vec2::new(32., 26.);
+
+ let previous_button = ui.add(egui::Button::new("⏮").min_size(min_button_size));
= if previous_button.clicked() {
= slider_value.0 = (slider_value.0 - 0.3).floor();
= }
=
= if time.is_paused() || time.relative_speed() != 1.0 {
- let play_button = ui.button("▶");
+ let play_button = ui.add(egui::Button::new("▶").min_size(min_button_size));
= if play_button.clicked() {
= time.unpause();
= time.set_relative_speed(1.0);
= };
= } else {
- let pause_button = ui.button("⏸");
+ let pause_button = ui.add(egui::Button::new("⏸").min_size(min_button_size));
= if pause_button.clicked() {
= time.pause();
= }
@@ -75,18 +77,18 @@ fn paint_ui(
=
= let next_speed = (time.relative_speed() * 2.0).min(128.0);
= let label = format!("⏩ ×{next_speed:.0}");
- let export_button = ui.button(label);
+ let export_button = ui.add(egui::Button::new(label).min_size(min_button_size));
= if export_button.clicked() {
= time.unpause();
= time.set_relative_speed(next_speed);
= }
=
- let next_button = ui.button("⏭");
+ let next_button = ui.add(egui::Button::new("⏭").min_size(min_button_size));
= if next_button.clicked() {
= slider_value.0 = (slider_value.0 + 0.3).ceil();
= }
=
- let export_button = ui.button("⏏");
+ let export_button = ui.add(egui::Button::new("⏏").min_size(min_button_size));
= if export_button.clicked() {
= let exported = pgsql_export::export(history.as_ref());
= // TODO: Save a fileFix: no roads in the initial snapshot
On by
The fix is more of a kludge, but seems to work, at the expense of exposing internals of the history module.
index b74cbd9..55e3859 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -111,7 +111,8 @@ pub struct Snapshot {
= pub roads: roads::Snapshot,
=}
=
-fn take_snapshot(world: &mut World) {
+// TODO: Find a way to coordinate with lay_initial_roads without exposing this system
+pub fn take_snapshot(world: &mut World) {
= // TODO: DRY on take_snapshot. Maybe a macro?
= let date = {
= let system = world.resource_scope(|_, systems: Mut<DateSystems>| systems.take_snapshot);index 620f17b..18b77db 100644
--- a/src/roads.rs
+++ b/src/roads.rs
@@ -1,5 +1,5 @@
=use crate::coordinates::{Coordinates, Direction};
-use crate::{GameState, PreloadedAssets, SimulationParameters};
+use crate::{history, GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -17,7 +17,11 @@ impl Plugin for RoadsPlugin {
= app.init_resource::<Roads>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_road_systems)
- .add_systems(OnEnter(GameState::Simulate), lay_initial_roads);
+ .add_systems(
+ OnEnter(GameState::Simulate),
+ // TODO: Avoid coupling with history systems
+ lay_initial_roads.before(history::take_snapshot),
+ );
= }
=}
=Changes to model sizes. Removed simple small house with a cube (kenney's model didn't make sense).
On by
index 76d6e62..6ccb6c5 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differAdded cube for supermarket block.
On by
index 6ccb6c5..cb1767d 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differCreate the people module with Savings component
On by
For now 10 people are spawned once at the startup, with savings between 100 and 1000 Oetters.
index c12076f..391f64c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@ mod exploration;
=mod ground;
=mod history;
=mod pgsql_export;
+mod population;
=mod roads;
=mod simulation;
=mod sun;
@@ -28,6 +29,7 @@ use districts::DistrictsPlugin;
=use exploration::ExplorationPlugin;
=use ground::GroundPlugin;
=use history::HistoryPlugin;
+use population::PopulationPlugin;
=use roads::RoadsPlugin;
=use simulation::SimulationPlugin;
=use sun::SunPlugin;
@@ -58,6 +60,7 @@ fn main() {
= .add_plugins(WorldInspectorPlugin::new())
= .add_plugins(SimulationPlugin)
= .add_plugins(ExplorationPlugin)
+ .add_plugins(PopulationPlugin)
= .add_systems(Startup, greet)
= .add_systems(Update, preload_assets.run_if(in_state(GameState::Loading)))
= .add_systems(Update, switch_states.run_if(on_event::<NewMonth>()))new file mode 100644
index 0000000..c042eb7
--- /dev/null
+++ b/src/population.rs
@@ -0,0 +1,25 @@
+use bevy::prelude::*;
+
+pub struct PopulationPlugin;
+
+impl Plugin for PopulationPlugin {
+ fn build(&self, app: &mut App) {
+ app.register_type::<Savings>()
+ .add_systems(Startup, setup_population);
+ }
+}
+
+fn setup_population(mut commands: Commands) {
+ info!("Hello from the population! Praise the Otter!");
+
+ for index in 1..=10 {
+ let name = format!("Person {index:03}");
+ commands.spawn((Person, Name::new(name), Savings(100.0 * index as f32)));
+ }
+}
+
+#[derive(Component)]
+struct Person;
+
+#[derive(Component, Reflect)]
+struct Savings(f32);Establish new districts when needed by the people
On by
Run the establish_new_districts system on every simulation frame, but only place new districts when there is more people then parcels and houses.
index a359854..d77c58e 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -15,8 +15,8 @@ impl Plugin for DatePlugin {
= app.insert_resource(Date(date))
= .register_type::<Timescale>()
= .init_resource::<Timescale>()
- .register_type::<FrameCounter>()
- .init_resource::<FrameCounter>()
+ .register_type::<SimulationFrameCounter>()
+ .init_resource::<SimulationFrameCounter>()
= .add_event::<NewMonth>()
= .add_systems(Startup, setup_date_display)
= .add_systems(Startup, register_date_systems)
@@ -46,7 +46,7 @@ impl Date {
=/// Count the frames simulated so far
=#[derive(Resource, Default, Debug, Reflect)]
=#[reflect(Resource)]
-struct FrameCounter(u8);
+pub struct SimulationFrameCounter(u8);
=
=#[derive(Resource, Reflect, InspectorOptions)]
=#[reflect(Resource, InspectorOptions)]
@@ -69,7 +69,7 @@ pub struct NewMonth;
=fn advance_simulation_date(
= mut date: ResMut<Date>,
= mut new_month: EventWriter<NewMonth>,
- mut framecounter: ResMut<FrameCounter>,
+ mut framecounter: ResMut<SimulationFrameCounter>,
= simulation: Res<SimulationParameters>,
=) {
= framecounter.0 = u8::rem_euclid(framecounter.0 + 1, simulation.frames_per_day);index ab5eaf6..727ad41 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,6 +1,7 @@
-use crate::buildings::building_class;
+use crate::buildings::{building_class, Building};
=use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
-use crate::date::NewMonth;
+use crate::date::SimulationFrameCounter;
+use crate::population::Person;
=use crate::roads::{RoadPlan, RoadsSystems};
=use crate::{GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
@@ -25,8 +26,10 @@ impl Plugin for DistrictsPlugin {
= .add_systems(OnEnter(GameState::Simulate), setup_parcels)
= .add_systems(
= Update,
- establish_new_districts
- .run_if(on_event::<NewMonth>().and_then(in_state(GameState::Simulate))),
+ establish_new_districts.run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
= )
= .add_systems(Update, spawn_new_districts);
= }
@@ -298,8 +301,19 @@ impl From<&District> for Rect {
=fn establish_new_districts(
= districts: Query<&District>,
= settings: Res<SimulationParameters>,
+ people: Query<&Person>,
+ parcels: Query<&Parcel>,
+ buildings: Query<&Building>,
= mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
+ let population = people.iter().count();
+ let capacity = buildings.iter().count() + parcels.iter().count();
+
+ if population < capacity {
+ return;
+ }
+ info!("Not enough living space ({population} / {capacity}). Planning a new district.");
+
= // TODO: Read from the asset (scene)
= const LENGTH: u32 = 18;
= const WIDTH: u32 = 30;index c042eb7..d53e614 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -19,7 +19,7 @@ fn setup_population(mut commands: Commands) {
=}
=
=#[derive(Component)]
-struct Person;
+pub struct Person;
=
=#[derive(Component, Reflect)]
-struct Savings(f32);
+pub struct Savings(f32);Construct buildings based on housing shortage.
On by
index 51f49de..6dfb813 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,6 +1,8 @@
=use crate::date::NewMonth;
+use crate::population::Person;
=use crate::districts::Parcel;
=use crate::{GameState, PreloadedAssets};
+use crate::date::SimulationFrameCounter;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -22,7 +24,10 @@ impl Plugin for BuildingsPlugin {
= .add_systems(
= Update,
= order_construction
- .run_if(on_event::<NewMonth>().and_then(in_state(GameState::Simulate))),
+ .run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
= )
= .add_systems(Update, receive_orders);
= }
@@ -143,31 +148,32 @@ pub struct Building {
=/// Issue a construction order, for the record and effect
=fn order_construction(
= mut build_events: EventWriter<ConstructionOrder>,
+ people: Query<&Person>,
= parcels: Query<(Entity, &Parcel, &GlobalTransform)>,
+ buildings: Query<&Building>,
= mut commands: Commands,
= models: Res<BuildingModels>,
=) {
= let count = parcels.iter().count();
= info!("There are {count} parcels now.");
=
- for class in models.classes() {
- info!("Let's build a {class}");
-
- // Filter parcel that matches this class
- for (entity, _, transform) in parcels.iter().filter(|(_, parcel, _)| parcel.class == class).take(5) {
- let models = &models.get_class(&class);
- let model = models.keys().choose(&mut rand::thread_rng()).unwrap();
-
- // Remove parcel
- commands.entity(entity).despawn();
-
- // Order the construction
- let description = BuildingDescription {
- transform: transform.to_owned().into(),
- model: model.to_string(),
- };
- build_events.send(ConstructionOrder(description));
- }
+ let population = people.iter().count();
+ let capacity = buildings.iter().count();
+ let housing_shortage = population - capacity;
+
+ for (entity, parcel, transform) in parcels.iter().take(housing_shortage) {
+ let models = &models.get_class(&parcel.class);
+ let model = models.keys().choose(&mut rand::thread_rng()).unwrap();
+
+ // Remove parcel
+ commands.entity(entity).despawn();
+
+ // Order the construction
+ let description = BuildingDescription {
+ transform: transform.to_owned().into(),
+ model: model.to_string(),
+ };
+ build_events.send(ConstructionOrder(description));
= }
=}
=index d53e614..c9e95a6 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -12,7 +12,7 @@ impl Plugin for PopulationPlugin {
=fn setup_population(mut commands: Commands) {
= info!("Hello from the population! Praise the Otter!");
=
- for index in 1..=10 {
+ for index in 1..=15 {
= let name = format!("Person {index:03}");
= commands.spawn((Person, Name::new(name), Savings(100.0 * index as f32)));
= }Immigrating 2 people per simulation frame.
On by
index 6dfb813..d2116a6 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,4 +1,3 @@
-use crate::date::NewMonth;
=use crate::population::Person;
=use crate::districts::Parcel;
=use crate::{GameState, PreloadedAssets};index c9e95a6..014ed78 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,18 +1,23 @@
=use bevy::prelude::*;
+use crate::date::SimulationFrameCounter;
+use crate::{GameState};
=
=pub struct PopulationPlugin;
=
=impl Plugin for PopulationPlugin {
= fn build(&self, app: &mut App) {
= app.register_type::<Savings>()
- .add_systems(Startup, setup_population);
+ .add_systems(Update, immigration.run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ));
= }
=}
=
-fn setup_population(mut commands: Commands) {
- info!("Hello from the population! Praise the Otter!");
+fn immigration(mut commands: Commands) {
+ info!("Hello from the population! Praise the Otter God!");
=
- for index in 1..=15 {
+ for index in 1..=2 {
= let name = format!("Person {index:03}");
= commands.spawn((Person, Name::new(name), Savings(100.0 * index as f32)));
= }Immigrating people only at 8am
On by
index d77c58e..99348e5 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -46,7 +46,7 @@ impl Date {
=/// Count the frames simulated so far
=#[derive(Resource, Default, Debug, Reflect)]
=#[reflect(Resource)]
-pub struct SimulationFrameCounter(u8);
+pub struct SimulationFrameCounter(pub u8);
=
=#[derive(Resource, Reflect, InspectorOptions)]
=#[reflect(Resource, InspectorOptions)]index 014ed78..04e68af 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -14,9 +14,14 @@ impl Plugin for PopulationPlugin {
= }
=}
=
-fn immigration(mut commands: Commands) {
+fn immigration(mut commands: Commands, frame: Res<SimulationFrameCounter>) {
= info!("Hello from the population! Praise the Otter God!");
=
+ // Only immigrate people at 8am
+ if frame.0 != 1 {
+ return;
+ }
+
= for index in 1..=2 {
= let name = format!("Person {index:03}");
= commands.spawn((Person, Name::new(name), Savings(100.0 * index as f32)));Format the code
On by
Pedro doesn't have a working code formatter yet.
index d2116a6..8cc3da1 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -1,7 +1,7 @@
-use crate::population::Person;
+use crate::date::SimulationFrameCounter;
=use crate::districts::Parcel;
+use crate::population::Person;
=use crate::{GameState, PreloadedAssets};
-use crate::date::SimulationFrameCounter;
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
@@ -22,11 +22,10 @@ impl Plugin for BuildingsPlugin {
= .add_systems(OnEnter(GameState::Simulate), setup_building_models)
= .add_systems(
= Update,
- order_construction
- .run_if(
- in_state(GameState::Simulate)
- .and_then(resource_changed::<SimulationFrameCounter>),
- ),
+ order_construction.run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
= )
= .add_systems(Update, receive_orders);
= }
@@ -159,7 +158,7 @@ fn order_construction(
= let population = people.iter().count();
= let capacity = buildings.iter().count();
= let housing_shortage = population - capacity;
-
+
= for (entity, parcel, transform) in parcels.iter().take(housing_shortage) {
= let models = &models.get_class(&parcel.class);
= let model = models.keys().choose(&mut rand::thread_rng()).unwrap();index 04e68af..2cf8e2c 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,16 +1,17 @@
-use bevy::prelude::*;
=use crate::date::SimulationFrameCounter;
-use crate::{GameState};
+use crate::GameState;
+use bevy::prelude::*;
=
=pub struct PopulationPlugin;
=
=impl Plugin for PopulationPlugin {
= fn build(&self, app: &mut App) {
- app.register_type::<Savings>()
- .add_systems(Update, immigration.run_if(
- in_state(GameState::Simulate)
- .and_then(resource_changed::<SimulationFrameCounter>),
- ));
+ app.register_type::<Savings>().add_systems(
+ Update,
+ immigration.run_if(
+ in_state(GameState::Simulate).and_then(resource_changed::<SimulationFrameCounter>),
+ ),
+ );
= }
=}
=Fix panic when housing shortage is negative
On by
An unsigned integer overflow. Saturating subtraction clamps it to 0.
index 8cc3da1..d927523 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -157,7 +157,7 @@ fn order_construction(
=
= let population = people.iter().count();
= let capacity = buildings.iter().count();
- let housing_shortage = population - capacity;
+ let housing_shortage = population.saturating_sub(capacity);
=
= for (entity, parcel, transform) in parcels.iter().take(housing_shortage) {
= let models = &models.get_class(&parcel.class);Implement the daily run condition
On by
It runs a given system once a day-month on a given hour. Should work in both simulation and exploration mode.
index 99348e5..ccd679e 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -162,3 +162,31 @@ fn register_date_systems(world: &mut World) {
= advance_simulation_date,
= });
=}
+
+/// A run condition that fires once a day on a given hour, or on the next opportunity
+///
+/// The hour is given as a floating point number between 0 (midnight) and 24:00
+/// (following midnight, exclusive). So 6.25 translates to 06:15.
+pub fn daily(hour: f32) -> impl FnMut(Option<Res<Date>>, Local<Option<DayMonth>>) -> bool {
+ // No funny business with overflowing values!
+ let hour = hour.rem_euclid(24.0);
+
+ move |date: Option<Res<Date>>, mut last_ran: Local<Option<DayMonth>>| {
+ let Some(date) = date else {
+ return false;
+ };
+
+ // On first run, pretend that it happened on previous daymonth
+ let previous = last_ran.unwrap_or(*date.0.clone().set_hour(hour).advance(-1.0));
+ let next = *previous.clone().advance(1.0);
+ let now = date.0.clone();
+
+ if now >= next {
+ // Do it, and pretend it happened on time
+ *last_ran = Some(*now.clone().set_hour(hour));
+ true
+ } else {
+ false
+ }
+ }
+}index 3c94f51..81aa8a8 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -90,6 +90,23 @@ impl DayMonth {
= Self { year, month: 0.0 }
= }
=
+ /// Set the time of day to a value between 0.0 (last midnight) to 24.0 (next midnight)
+ ///
+ /// Giving a negative value or value above 24.0 should do the sensible thing,
+ /// i.e. decrement or increment the month and year.
+ pub fn set_hour(&mut self, hour: f32) -> &mut Self {
+ self.set_time_of_day(hour / 24.0)
+ }
+
+ /// Set the time of day to a value between 0.0 (last midnight) to 1.0 (next midnight)
+ ///
+ /// Giving a negative value or value above 1.0 should do the sensible thing,
+ /// i.e. decrement or increment the month and year.
+ pub fn set_time_of_day(&mut self, time_of_day: f32) -> &mut Self {
+ self.month = self.month.floor();
+ self.advance(time_of_day)
+ }
+
= pub fn advance(&mut self, duration: f32) -> &mut Self {
= let month = self.month + duration;
=index 391f64c..3915316 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -102,7 +102,7 @@ impl Default for SimulationParameters {
= sun_gap: 200.,
= sun_elevation: 600.,
= first_year: 1865,
- years: 2,
+ years: 20,
= frame_offset: 2.0 * HOUR,
= frames_per_day: 6,
= }index 2cf8e2c..0e97bfa 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,3 +1,4 @@
+use crate::date;
=use crate::date::SimulationFrameCounter;
=use crate::GameState;
=use bevy::prelude::*;
@@ -9,21 +10,19 @@ impl Plugin for PopulationPlugin {
= app.register_type::<Savings>().add_systems(
= Update,
= immigration.run_if(
- in_state(GameState::Simulate).and_then(resource_changed::<SimulationFrameCounter>),
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>)
+ .and_then(date::daily(6.0)),
= ),
= );
= }
=}
=
-fn immigration(mut commands: Commands, frame: Res<SimulationFrameCounter>) {
- info!("Hello from the population! Praise the Otter God!");
+fn immigration(mut commands: Commands) {
+ let number = 20;
+ info!("{number} people immigrated to Otterhide.");
=
- // Only immigrate people at 8am
- if frame.0 != 1 {
- return;
- }
-
- for index in 1..=2 {
+ for index in 1..=number {
= let name = format!("Person {index:03}");
= commands.spawn((Person, Name::new(name), Savings(100.0 * index as f32)));
= }Rename daily run condition to past_hour
On by
It reads better in context of Bevy's run_if, as in
do_groceries.run_if(past_hour(7.5))index ccd679e..16132f0 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -167,7 +167,7 @@ fn register_date_systems(world: &mut World) {
=///
=/// The hour is given as a floating point number between 0 (midnight) and 24:00
=/// (following midnight, exclusive). So 6.25 translates to 06:15.
-pub fn daily(hour: f32) -> impl FnMut(Option<Res<Date>>, Local<Option<DayMonth>>) -> bool {
+pub fn past_hour(hour: f32) -> impl FnMut(Option<Res<Date>>, Local<Option<DayMonth>>) -> bool {
= // No funny business with overflowing values!
= let hour = hour.rem_euclid(24.0);
=index 0e97bfa..75a005e 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -12,7 +12,7 @@ impl Plugin for PopulationPlugin {
= immigration.run_if(
= in_state(GameState::Simulate)
= .and_then(resource_changed::<SimulationFrameCounter>)
- .and_then(date::daily(6.0)),
+ .and_then(date::past_hour(6.0)),
= ),
= );
= }Linked and added more varied buildings to districts (supermarket, school, library). Added Library. Resized supermarket and school.
On by
index cb1767d..53136f0 100644
Binary files a/art/buildings.blend and b/art/buildings.blend differindex d3320fe..9e5e9f5 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 2ffa7ca..4f7b2fd 100644
Binary files a/assets/buildings.glb and b/assets/buildings.glb differindex b03ac12..1148872 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differGive people names and sex
On by
index 3915316..bdc6db3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,7 @@ mod districts;
=mod exploration;
=mod ground;
=mod history;
+mod names;
=mod pgsql_export;
=mod population;
=mod roads;new file mode 100644
index 0000000..80a8528
--- /dev/null
+++ b/src/names.rs
@@ -0,0 +1,496 @@
+use rand::{seq::SliceRandom, thread_rng};
+
+use crate::population::Sex;
+
+pub fn random_first_name(sex: &Sex) -> String {
+ let names = match sex {
+ &Sex::Male => MALE_NAMES,
+ &Sex::Female => FEMALE_NAMES,
+ };
+
+ names.choose(&mut thread_rng()).unwrap().to_string()
+}
+
+pub fn random_last_name() -> String {
+ LAST_NAMES.choose(&mut thread_rng()).unwrap().to_string()
+}
+
+type NamesList = &'static [&'static str];
+
+const LAST_NAMES: NamesList = &[
+ "Aardvark",
+ "Albatross",
+ "Antelope",
+ "Baboon",
+ "Badger",
+ "Bat",
+ "Bear",
+ "Beaver",
+ "Bison",
+ "Bittern",
+ "Blackbird",
+ "Boar",
+ "Bobolink",
+ "Buffalo",
+ "Bunting",
+ "Bushtit",
+ "Camel",
+ "Capybara",
+ "Cardinal",
+ "Cat",
+ "Cheetah",
+ "Chickadee",
+ "Chimpanzee",
+ "Chinchilla",
+ "Cormorant",
+ "Cougar",
+ "Cow",
+ "Cowbird",
+ "Coyote",
+ "Crane",
+ "Creeper",
+ "Crocodile",
+ "Crossbill",
+ "Crow",
+ "Deer",
+ "Dingo",
+ "Dipper",
+ "Dog",
+ "Donkey",
+ "Dove",
+ "Duck",
+ "Eagle",
+ "Egret",
+ "Falcon",
+ "Ferret",
+ "Finch",
+ "Flamingo",
+ "Flycatcher",
+ "Fox",
+ "Frog",
+ "Gazelle",
+ "Gerbil",
+ "Giraffe",
+ "Goat",
+ "Goldfinch",
+ "Goose",
+ "Gorilla",
+ "Grackle",
+ "Grebe",
+ "Grosbeak",
+ "Hamster",
+ "Hawk",
+ "Hen",
+ "Heron",
+ "Hippopotamus",
+ "Hyena",
+ "Ibex",
+ "Ibis",
+ "Jackal",
+ "Jaguar",
+ "Jay",
+ "Junco",
+ "Kangaroo",
+ "Kingfisher",
+ "Kinglets",
+ "Koala",
+ "Lark",
+ "Lemur",
+ "Leopard",
+ "Lion",
+ "Lizard",
+ "Llama",
+ "Longspur",
+ "Loon",
+ "Lynx",
+ "Macaw",
+ "Magpie",
+ "Mandrill",
+ "Martin",
+ "Meadowlark",
+ "Mink",
+ "Mole",
+ "Monkey",
+ "Moose",
+ "Mouse",
+ "Mule",
+ "Nuthatch",
+ "Ocelot",
+ "Opossum",
+ "Orangutan",
+ "Oriole",
+ "Otter",
+ "Owl",
+ "Ox",
+ "Panda",
+ "Panther",
+ "Parrot",
+ "Peacock",
+ "Pelican",
+ "Penguin",
+ "Pig",
+ "Pigeon",
+ "Pipit",
+ "Possum",
+ "Puma",
+ "Rabbit",
+ "Raccoon",
+ "Raven",
+ "Redpoll",
+ "Reindeer",
+ "Rhinoceros",
+ "Robin",
+ "Salamander",
+ "Seagull",
+ "Seal",
+ "Shark",
+ "Sheep",
+ "Shrike",
+ "Siskin",
+ "Skunk",
+ "Sloth",
+ "Snail",
+ "Snake",
+ "Snowbunting",
+ "Sparrow",
+ "Spider",
+ "Spoonbill",
+ "Squirrel",
+ "Starling",
+ "Stork",
+ "Swallow",
+ "Swan",
+ "Tanager",
+ "Thrush",
+ "Tiger",
+ "Titmouse",
+ "Toad",
+ "Tortoise",
+ "Towhee",
+ "Turkey",
+ "Turtle",
+ "Vireo",
+ "Wagtails",
+ "Wallaby",
+ "Warbler",
+ "Waxwing",
+ "Weasel",
+ "Whale",
+ "Wolf",
+ "Wombat",
+ "Woodpecker",
+ "Wren",
+ "Wrentit",
+ "Zebra",
+];
+
+const FEMALE_NAMES: &'static [&'static str] = &[
+ "Aaliyah",
+ "Abigail",
+ "Adriana",
+ "Aisha",
+ "Alice",
+ "Allison",
+ "Alyssa",
+ "Amanda",
+ "Amber",
+ "Amelia",
+ "Amy",
+ "Angela",
+ "Anna",
+ "Ariana",
+ "Ashley",
+ "Aubrey",
+ "Avery",
+ "Barbara",
+ "Betty",
+ "Bianca",
+ "Brenda",
+ "Briana",
+ "Brianna",
+ "Brittany",
+ "Caitlin",
+ "Camille",
+ "Carla",
+ "Carmen",
+ "Carol",
+ "Caroline",
+ "Carolyn",
+ "Carrie",
+ "Cassandra",
+ "Catherine",
+ "Cheryl",
+ "Chloe",
+ "Christina",
+ "Christine",
+ "Christy",
+ "Claire",
+ "Clarice",
+ "Cynthia",
+ "Daisy",
+ "Deborah",
+ "Debra",
+ "Diane",
+ "Donna",
+ "Doris",
+ "Dorothy",
+ "Eden",
+ "Eleanor",
+ "Elena",
+ "Elizabeth",
+ "Emily",
+ "Emma",
+ "Erika",
+ "Erin",
+ "Evelyn",
+ "Frances",
+ "Gabriela",
+ "Gabrielle",
+ "Gloria",
+ "Grace",
+ "Gretchen",
+ "Gwen",
+ "Hannah",
+ "Heather",
+ "Helen",
+ "Irene",
+ "Isabella",
+ "Jackie",
+ "Jada",
+ "Jamie",
+ "Janet",
+ "Janice",
+ "Jasmine",
+ "Jenna",
+ "Jennie",
+ "Jennifer",
+ "Jessica",
+ "Jillian",
+ "Joan",
+ "Jocelyn",
+ "Joyce",
+ "Judith",
+ "Judy",
+ "Julia",
+ "Julie",
+ "Kacey",
+ "Karen",
+ "Katherine",
+ "Kathleen",
+ "Katie",
+ "Kayla",
+ "Kim",
+ "Kimberly",
+ "Kristen",
+ "Kristina",
+ "Lacey",
+ "Laura",
+ "Lauren",
+ "Leah",
+ "Lena",
+ "Lily",
+ "Linda",
+ "Lisa",
+ "Lorena",
+ "Lori",
+ "Lourdes",
+ "Madison",
+ "Margaret",
+ "Maria",
+ "Mariah",
+ "Mary",
+ "Megan",
+ "Melissa",
+ "Mia",
+ "Michelle",
+ "Mildred",
+ "Molly",
+ "Monica",
+ "Nancy",
+ "Natalia",
+ "Natalie",
+ "Nia",
+ "Nicole",
+ "Olivia",
+ "Paige",
+ "Pamela",
+ "Patricia",
+ "Rachel",
+ "Rebecca",
+ "Rose",
+ "Roxanne",
+ "Ruth",
+ "Sadie",
+ "Samantha",
+ "Sandra",
+ "Sara",
+ "Sarah",
+ "Selena",
+ "Sharon",
+ "Shelly",
+ "Shirley",
+ "Sierra",
+ "Sophia",
+ "Stacy",
+ "Stephanie",
+ "Susan",
+ "Tammy",
+ "Tara",
+ "Teresa",
+ "Tessa",
+ "Tiffany",
+ "Trisha",
+ "Violet",
+ "Virginia",
+ "Wendy",
+ "Zoe",
+];
+
+const MALE_NAMES: NamesList = &[
+ "Aaron",
+ "Abel",
+ "Adam",
+ "Adrian",
+ "Ahmed",
+ "Aiden",
+ "Alexander",
+ "Alfred",
+ "Andrew",
+ "Angel",
+ "Anthony",
+ "Arnold",
+ "Asher",
+ "Austin",
+ "Ayden",
+ "Benjamin",
+ "Billy",
+ "Brandon",
+ "Brayden",
+ "Brian",
+ "Bruce",
+ "Bryan",
+ "Caleb",
+ "Cameron",
+ "Carl",
+ "Carter",
+ "Charles",
+ "Christian",
+ "Christopher",
+ "Cooper",
+ "Daniel",
+ "David",
+ "Dennis",
+ "Derick",
+ "Dominic",
+ "Don",
+ "Donald",
+ "Douglas",
+ "Dylan",
+ "Easton",
+ "Eduardo",
+ "Edward",
+ "Eli",
+ "Elias",
+ "Elijah",
+ "Eric",
+ "Ethan",
+ "Evan",
+ "Ezra",
+ "Frank",
+ "Franklin",
+ "Frederick",
+ "Gabriel",
+ "Gary",
+ "George",
+ "Gerald",
+ "Grayson",
+ "Gregory",
+ "Harry",
+ "Henry",
+ "Hunter",
+ "Ian",
+ "Isaac",
+ "Jack",
+ "Jackson",
+ "Jacob",
+ "James",
+ "Jason",
+ "Jaxon",
+ "Jaxson",
+ "Jayden",
+ "Jeffrey",
+ "Jeremiah",
+ "Jeremy",
+ "Jerry",
+ "Jesse",
+ "Joe",
+ "John",
+ "Jonathan",
+ "Jordan",
+ "Jose",
+ "Joseph",
+ "Joshua",
+ "Josiah",
+ "Juan",
+ "Julian",
+ "Justin",
+ "Kenneth",
+ "Kevin",
+ "Landon",
+ "Larry",
+ "Lawrence",
+ "Levi",
+ "Liam",
+ "Logan",
+ "Louis",
+ "Lucas",
+ "Luke",
+ "Mark",
+ "Martin",
+ "Mason",
+ "Mateo",
+ "Matthew",
+ "Maverick",
+ "Michael",
+ "Mohammed",
+ "Nathan",
+ "Nicholas",
+ "Nicolas",
+ "Noah",
+ "Oliver",
+ "Omar",
+ "Owen",
+ "Patrick",
+ "Paul",
+ "Peter",
+ "Philip",
+ "Rafael",
+ "Randy",
+ "Raymond",
+ "Richard",
+ "Rick",
+ "Robert",
+ "Roger",
+ "Ronald",
+ "Ryan",
+ "Samuel",
+ "Sandra",
+ "Santiago",
+ "Scott",
+ "Sean",
+ "Sebastian",
+ "Stephen",
+ "Steven",
+ "Theodore",
+ "Thomas",
+ "Timothy",
+ "Tyler",
+ "Victor",
+ "Walter",
+ "Warren",
+ "Wayne",
+ "William",
+ "Wyatt",
+ "Xavier",
+ "Zachary",
+];index 75a005e..345c74a 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,20 +1,27 @@
=use crate::date;
=use crate::date::SimulationFrameCounter;
+use crate::names;
=use crate::GameState;
=use bevy::prelude::*;
+use rand::prelude::*;
+use std::fmt::Display;
=
=pub struct PopulationPlugin;
=
=impl Plugin for PopulationPlugin {
= fn build(&self, app: &mut App) {
- app.register_type::<Savings>().add_systems(
- Update,
- immigration.run_if(
- in_state(GameState::Simulate)
- .and_then(resource_changed::<SimulationFrameCounter>)
- .and_then(date::past_hour(6.0)),
- ),
- );
+ app.register_type::<Person>()
+ .register_type::<Savings>()
+ .register_type::<Sex>()
+ .register_type::<PersonName>()
+ .add_systems(
+ Update,
+ immigration.run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>)
+ .and_then(date::past_hour(6.0)),
+ ),
+ );
= }
=}
=
@@ -23,13 +30,68 @@ fn immigration(mut commands: Commands) {
= info!("{number} people immigrated to Otterhide.");
=
= for index in 1..=number {
- let name = format!("Person {index:03}");
- commands.spawn((Person, Name::new(name), Savings(100.0 * index as f32)));
+ let sex = random::<Sex>();
+ let name = PersonName::get_random(&sex);
+
+ commands
+ .spawn(Person)
+ .insert(Name::new(name.to_string()))
+ .insert(sex)
+ .insert(name)
+ .insert(Savings(100.0 * index as f32));
= }
=}
=
-#[derive(Component)]
+#[derive(Component, Debug, Reflect)]
=pub struct Person;
=
=#[derive(Component, Reflect)]
=pub struct Savings(f32);
+
+#[derive(Component, Reflect, Debug)]
+pub enum Sex {
+ Male,
+ Female,
+}
+
+impl Distribution<Sex> for rand::distributions::Standard {
+ fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Sex {
+ if rng.gen_bool(0.5) {
+ Sex::Male
+ } else {
+ Sex::Female
+ }
+ }
+}
+
+#[derive(Component, Reflect, Debug)]
+pub struct PersonName {
+ first: String,
+ second: String,
+ third: Option<String>,
+}
+
+impl PersonName {
+ pub fn get_random(sex: &Sex) -> Self {
+ Self {
+ first: names::random_first_name(sex),
+ second: names::random_last_name(),
+ third: None,
+ }
+ }
+}
+
+impl Display for PersonName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let PersonName {
+ first,
+ second,
+ third,
+ } = self;
+
+ match third {
+ None => write!(f, "{first} de {second}"),
+ Some(third) => write!(f, "{first} de {second}-{third}"),
+ }
+ }
+}Color code district models, correct sizing a bit
On by
Two of the districts were not aligned or had wrong sizes specified in names (ignoring the 5m padding for roads).
index 9e5e9f5..f5536e1 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 1148872..70d762f 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differUse all district models from .blend / .glb files
On by
When establishing a new district, use a random model.
index 727ad41..c36156a 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -7,9 +7,11 @@ use crate::{GameState, PreloadedAssets, SimulationParameters};
=use bevy::ecs::system::SystemId;
=use bevy::gltf::Gltf;
=use bevy::prelude::*;
+use bevy::utils::HashMap;
=use itertools::Itertools;
=use rand::seq::IteratorRandom;
=use rand::thread_rng;
+use regex::Regex;
=use std::borrow::Borrow;
=use std::f32::consts::FRAC_PI_2;
=use std::iter;
@@ -21,9 +23,10 @@ impl Plugin for DistrictsPlugin {
= fn build(&self, app: &mut App) {
= app.add_event::<NewDistrictEstablished>()
= .register_type::<Parcel>()
+ .init_resource::<DistrictModels>()
= .add_systems(Startup, register_district_systems)
= .add_systems(Startup, setup_assets)
- .add_systems(OnEnter(GameState::Simulate), setup_parcels)
+ .add_systems(OnEnter(GameState::Simulate), setup_districts)
= .add_systems(
= Update,
= establish_new_districts.run_if(
@@ -50,59 +53,97 @@ fn setup_assets(
= commands.insert_resource(DistrictAssets(handle));
=}
=
-fn setup_parcels(
+pub fn district_size(name: &str) -> Option<(u32, u32)> {
+ let district_name_pattern =
+ Regex::new(r"^district-(?<length>\d+)x(?<width>\d+)-(?<variant>.+)$").unwrap();
+ district_name_pattern.captures(name).and_then(|captured| {
+ let length: u32 = captured["length"].parse().unwrap();
+ let width: u32 = captured["width"].parse().unwrap();
+ // TODO: Consider renaming scenes in blender to use 10m units
+ // i.e. instead of district-180x300 call it district-18x30
+ Some((length / 10, width / 10))
+ })
+}
+
+fn setup_districts(
= districts_assets: Res<DistrictAssets>,
= assets: Res<Assets<Gltf>>,
= mut scenes: ResMut<Assets<Scene>>,
+ mut descriptions: ResMut<DistrictModels>,
=) {
= let districts = assets.get(&districts_assets.0).unwrap();
=
= // TODO: Do it for every scene
- let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
-
- let scene = scenes.get_mut(scene_handle).unwrap();
-
- let root = scene
- .world
- .query_filtered::<Entity, Without<Parent>>()
- .iter(&scene.world)
- .next()
- .unwrap();
-
- let mut query = scene.world.query::<(Entity, &Name, &Transform, &Parent)>();
- let parcels: Vec<(Entity, String, Transform)> = query
- .iter(&scene.world)
- .filter_map(|(entity, name, transform, parent)| {
- // Only consider direct first generation to avoid nested entities
- // with names matching the pattern being taken for parcels
- if parent.get() != root {
- debug!("Skipping indirect descendant {name}");
- return None;
- };
- // Check if the name matches pattern
- building_class(name).map(|class| (entity, class, *transform))
- })
- .collect_vec();
+ for (name, scene_handle) in districts.named_scenes.iter() {
+ info!("Processing district {name}");
+ let Some((length, width)) = district_size(name) else {
+ panic!("The size could not be determined from the district's name: {name}");
+ };
=
- for (entity, class, transform) in parcels {
- info!("Processing parcel {entity:?}, class {class}");
+ let scene = scenes.get_mut(scene_handle.clone()).unwrap();
=
- scene
+ let root = scene
= .world
- .spawn(SpatialBundle {
- transform,
- visibility: Visibility::Visible,
- ..default()
+ .query_filtered::<Entity, Without<Parent>>()
+ .iter(&scene.world)
+ .next()
+ .unwrap();
+
+ let mut query = scene.world.query::<(Entity, &Name, &Transform, &Parent)>();
+ let parcels: Vec<(Entity, String, Transform)> = query
+ .iter(&scene.world)
+ .filter_map(|(entity, name, transform, parent)| {
+ // Only consider direct first generation to avoid nested entities
+ // with names matching the pattern being taken for parcels
+ if parent.get() != root {
+ debug!("Skipping indirect descendant {name}");
+ return None;
+ };
+ // Check if the name matches pattern
+ building_class(name).map(|class| (entity, class, *transform))
= })
- .insert(Parcel { class });
- if let Some(entity) = scene.world.get_entity_mut(entity) {
- entity.despawn_recursive();
- } else {
- warn!("This entity does not exist!");
+ .collect_vec();
+
+ for (entity, class, transform) in parcels {
+ debug!("Processing parcel {entity:?}, class {class}");
+
+ scene
+ .world
+ .spawn(SpatialBundle {
+ transform,
+ visibility: Visibility::Visible,
+ ..default()
+ })
+ .insert(Parcel { class });
+ if let Some(entity) = scene.world.get_entity_mut(entity) {
+ entity.despawn_recursive();
+ } else {
+ warn!("This entity does not exist!");
+ };
+ }
+
+ let description = DistrictModel {
+ model: scene_handle.clone(),
+ width,
+ length,
= };
+
+ descriptions.0.insert(name.to_string(), description);
= }
+
+ info!("Districts processed: {descriptions:?}");
=}
=
+#[derive(Debug)]
+struct DistrictModel {
+ model: Handle<Scene>,
+ width: u32,
+ length: u32,
+}
+
+#[derive(Resource, Debug, Default)]
+struct DistrictModels(HashMap<String, DistrictModel>);
+
=#[derive(Component, Reflect, Debug)]
=#[reflect(Component)]
=pub struct Parcel {
@@ -112,11 +153,11 @@ pub struct Parcel {
=/// This event represents a decision to construct a new building.
=///
=/// It will be stored in the history, and will result in spawning a new building.
-#[derive(Event, Clone, Copy, Debug)]
+#[derive(Event, Clone, Debug)]
=pub struct NewDistrictEstablished(pub District);
=
=/// A tag for district entities
-#[derive(Component, Debug, Clone, Copy)]
+#[derive(Component, Debug, Clone)]
=pub struct District {
= /// Coordinates of the north-west corner
= pub origin: Coordinates,
@@ -126,6 +167,8 @@ pub struct District {
= pub width: u32,
= /// How is the district rotated compared to the model
= rotation: Rotation,
+ /// Which model to use
+ model: String,
=}
=
=#[derive(Component, Debug, Clone, Copy)]
@@ -169,11 +212,12 @@ impl From<isize> for Rotation {
=}
=
=impl District {
- pub fn new(origin: Coordinates, length: u32, width: u32) -> Self {
+ pub fn new(origin: Coordinates, length: u32, width: u32, model: String) -> Self {
= Self {
= origin,
= length,
= width,
+ model,
= rotation: Rotation::None,
= }
= }
@@ -304,6 +348,7 @@ fn establish_new_districts(
= people: Query<&Person>,
= parcels: Query<&Parcel>,
= buildings: Query<&Building>,
+ descriptions: Res<DistrictModels>,
= mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
= let population = people.iter().count();
@@ -314,9 +359,8 @@ fn establish_new_districts(
= }
= info!("Not enough living space ({population} / {capacity}). Planning a new district.");
=
- // TODO: Read from the asset (scene)
- const LENGTH: u32 = 18;
- const WIDTH: u32 = 30;
+ // TODO: Use some smarter heuristics to choose the district
+ let (model, description) = descriptions.0.iter().choose(&mut thread_rng()).unwrap();
=
= let corners = districts
= .iter()
@@ -325,26 +369,40 @@ fn establish_new_districts(
= .unique();
=
= let candidates = corners
- .map(|corner| District::new(corner, LENGTH, WIDTH))
+ .map(|corner| {
+ District::new(
+ corner,
+ description.length,
+ description.width,
+ model.to_string(),
+ )
+ })
= .flat_map(|candidate| {
= [
- *candidate.clone().rotate(Rotation::None),
- *candidate.clone().rotate(Rotation::CW90),
- *candidate.clone().rotate(Rotation::CW180),
- *candidate.clone().rotate(Rotation::CW270),
+ candidate.clone().rotate(Rotation::None).to_owned(),
+ candidate.clone().rotate(Rotation::CW90).to_owned(),
+ candidate.clone().rotate(Rotation::CW180).to_owned(),
+ candidate.clone().rotate(Rotation::CW270).to_owned(),
= ]
= .into_iter()
= })
= .flat_map(|candidate| {
= let District { length, width, .. } = candidate;
= [
- candidate,
- *candidate.clone().shift(width as i32, &Direction::North),
- *candidate
+ candidate.clone(),
+ candidate
= .clone()
= .shift(width as i32, &Direction::North)
- .shift(length as i32, &Direction::West),
- *candidate.clone().shift(length as i32, &Direction::West),
+ .to_owned(),
+ candidate
+ .clone()
+ .shift(width as i32, &Direction::North)
+ .shift(length as i32, &Direction::West)
+ .to_owned(),
+ candidate
+ .clone()
+ .shift(length as i32, &Direction::West)
+ .to_owned(),
= ]
= .into_iter()
= })
@@ -389,14 +447,14 @@ fn spawn_new_districts(
= systems: Res<DistrictsSystems>,
=) {
= for NewDistrictEstablished(district) in new_districts.read() {
- commands.run_system_with_input(systems.construct_district, *district);
+ commands.run_system_with_input(systems.construct_district, district.clone());
= }
=}
=
=pub type Snapshot = Vec<District>;
=
=pub fn take_snapshot(districts: Query<&District>) -> Snapshot {
- districts.iter().copied().collect()
+ districts.iter().cloned().collect()
=}
=
=fn rollback(
@@ -445,9 +503,7 @@ fn construct_district(
= info!("Spawning a district: {district:?}");
=
= let districts = assets.get(&districts_assets.0).unwrap();
-
- // TODO: Do it for every scene
- let scene_handle = districts.named_scenes.get("district-180x300-a").unwrap();
+ let scene_handle = districts.named_scenes.get(&district.model).unwrap();
=
= // Rotate and translate the district model
= // TODO: Move the transform logic below to a District::apply_transform method or something like that
@@ -477,7 +533,7 @@ fn construct_district(
= transform,
= ..default()
= })
- .insert(district)
+ .insert(district.clone())
= .insert(Name::new("District"));
=
= commands.run_system_with_input(
@@ -498,6 +554,7 @@ mod district_tests {
= Coordinates::new(Longitude::new(-3), Latitude::new(-2)),
= 13,
= 7,
+ String::default(),
= );
=
= assert_eq!(district.east(), Longitude::new(10));
@@ -526,7 +583,7 @@ mod district_tests {
=
= #[test]
= fn into_rect() {
- let district = District::new(Coordinates::default(), 10, 20);
+ let district = District::new(Coordinates::default(), 10, 20, String::default());
= let rect = Rect::from(&district);
= assert_eq!(rect.min, Vec2::ZERO);
= assert_eq!(rect.max, Vec2::new(100.0, 200.0));
@@ -535,6 +592,7 @@ mod district_tests {
= Coordinates::new(Longitude::new(10), Latitude::new(5)),
= 10,
= 20,
+ String::default(),
= );
= let rect = Rect::from(&district);
= assert_eq!(rect.min, Vec2::new(100.0, 50.0));
@@ -543,13 +601,14 @@ mod district_tests {
=
= #[test]
= fn center() {
- let district = District::new(Coordinates::default(), 10, 20);
+ let district = District::new(Coordinates::default(), 10, 20, String::default());
= assert_eq!(district.center(), Vec3::new(50.0, 0.0, 100.0));
=
= let district = District::new(
= Coordinates::new(Longitude::new(10), Latitude::new(5)),
= 10,
= 15,
+ String::default(),
= );
= assert_eq!(district.center(), Vec3::new(150.0, 0.0, 125.0));
= }Fix a bug where sometimes districts would overlap
On by
It sometimes happened when two districts were created in subsequent simulation frames. The bug was due to not deterministic order of systems application. If in the frame following establishment of new district the plan_new_district would be executed before apply_new_district, the previously planned district would not be taken into account when counting housing shortage (i.e. the shortage would always be detected and new district would be planned), and more importantly in placement algorithm. So two districts would be established one after the other, when really only one was needed, and sometimes they would overlap.
The fix is to put a scheduling constraint to always run planing after the implementation, so when planning, the new district is already spawned.
While working on it I also realized that DistrictModel doesn't need to hold a hendle to the scene, as it can be determined from the District::model. The naming seems confusing and probably needs some refactoring.
index c36156a..da98b91 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -29,12 +29,14 @@ impl Plugin for DistrictsPlugin {
= .add_systems(OnEnter(GameState::Simulate), setup_districts)
= .add_systems(
= Update,
- establish_new_districts.run_if(
- in_state(GameState::Simulate)
- .and_then(resource_changed::<SimulationFrameCounter>),
- ),
+ establish_new_districts
+ .after(implement_new_districts)
+ .run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
= )
- .add_systems(Update, spawn_new_districts);
+ .add_systems(Update, implement_new_districts);
= }
=}
=
@@ -122,11 +124,7 @@ fn setup_districts(
= };
= }
=
- let description = DistrictModel {
- model: scene_handle.clone(),
- width,
- length,
- };
+ let description = DistrictModel { width, length };
=
= descriptions.0.insert(name.to_string(), description);
= }
@@ -136,7 +134,6 @@ fn setup_districts(
=
=#[derive(Debug)]
=struct DistrictModel {
- model: Handle<Scene>,
= width: u32,
= length: u32,
=}
@@ -156,6 +153,7 @@ pub struct Parcel {
=#[derive(Event, Clone, Debug)]
=pub struct NewDistrictEstablished(pub District);
=
+// TODO: Divide into smaller components.
=/// A tag for district entities
=#[derive(Component, Debug, Clone)]
=pub struct District {
@@ -407,7 +405,7 @@ fn establish_new_districts(
= .into_iter()
= })
= .filter(|candidate| {
- info!("Trying corner {candidate:?}");
+ info!("Trying to place a district {candidate:?}");
=
= // TODO: Extract into Latitude::same_hemisphere method
= if i32::from(candidate.east()) * i32::from(candidate.west()) < 0 {
@@ -440,8 +438,10 @@ fn establish_new_districts(
= new_districts.send(NewDistrictEstablished(selected));
=}
=
-// TODO: Rename
-fn spawn_new_districts(
+/// Process NewDistrictEstablished events
+///
+/// The events might be coming from establish_new_districts or replay_historical_events
+fn implement_new_districts(
= mut commands: Commands,
= mut new_districts: EventReader<NewDistrictEstablished>,
= systems: Res<DistrictsSystems>,
@@ -629,5 +629,24 @@ mod district_tests {
= let a = Rect::new(0., 0., 2., 2.);
= let b = Rect::new(2., 0., 4., 2.);
= assert!(a.intersect(b).is_empty());
+
+ // Contained
+ let a = District {
+ origin: Coordinates::new(Longitude::new(-60), Latitude::new(-49)),
+ length: 30,
+ width: 49,
+ rotation: Rotation::CW270,
+ model: "district-490x300-a".to_string(),
+ };
+ let b = District {
+ origin: Coordinates::new(Longitude::new(-60), Latitude::new(-18)),
+ length: 30,
+ width: 18,
+ rotation: Rotation::CW90,
+ model: "district-180x300-b".to_string(),
+ };
+
+ assert!(a.overlaps(&b));
+ assert!(b.overlaps(&a));
= }
=}Rename: (establish -> plan)_new_districts
On by
This name is less ambiguous and reflects better what the system does. It just makes a plan for a new district. The plan is later (on the next frame) implemented by the implement_new_districts system.
index da98b91..cabadda 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -29,12 +29,10 @@ impl Plugin for DistrictsPlugin {
= .add_systems(OnEnter(GameState::Simulate), setup_districts)
= .add_systems(
= Update,
- establish_new_districts
- .after(implement_new_districts)
- .run_if(
- in_state(GameState::Simulate)
- .and_then(resource_changed::<SimulationFrameCounter>),
- ),
+ plan_new_districts.after(implement_new_districts).run_if(
+ in_state(GameState::Simulate)
+ .and_then(resource_changed::<SimulationFrameCounter>),
+ ),
= )
= .add_systems(Update, implement_new_districts);
= }
@@ -340,7 +338,7 @@ impl From<&District> for Rect {
= }
=}
=
-fn establish_new_districts(
+fn plan_new_districts(
= districts: Query<&District>,
= settings: Res<SimulationParameters>,
= people: Query<&Person>,Rename a parameter
On by
index cabadda..05ae3b0 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -496,7 +496,7 @@ fn construct_district(
= districts_assets: Res<DistrictAssets>,
= assets: Res<Assets<Gltf>>,
= mut commands: Commands,
- road_constructors: Res<RoadsSystems>,
+ road_systems: Res<RoadsSystems>,
=) {
= info!("Spawning a district: {district:?}");
=
@@ -535,7 +535,7 @@ fn construct_district(
= .insert(Name::new("District"));
=
= commands.run_system_with_input(
- road_constructors.implement_road_plan,
+ road_systems.implement_road_plan,
= district.plan_border_roads(),
= );
=}Merge Longitude and Latitude into Coordinate
On by
There was a lot of code repetition for those two types. Now there is a single Coordinate, parameterized with a type that must implement Dimension. Two such types are Longitude and Latitude. They don't carry any value, and essentially serve as compile time markers, preventing the accidental use of one coordinate in place of the other.
Everything should work the same, just without the duplicated code for two different types, that have the same behavior.
index 6888df0..81bc1ea 100644
--- a/src/coordinates.rs
+++ b/src/coordinates.rs
@@ -1,17 +1,71 @@
=use bevy::math::{Vec2, Vec3};
-use derive_more::{AsRef, From, Into};
+use derive_more::{AsRef, From};
+use std::borrow::Borrow;
=use std::fmt::Display;
-use std::ops::{Add, AddAssign, SubAssign};
+use std::marker::PhantomData;
+use std::ops::{Add, AddAssign, Sub, SubAssign};
+
+// TODO: Make a derive macro for Dimension
+/// A generic dimension, like X, Y, Latitude, Longitude, Altitude, Time, etc.
+pub trait Dimension {}
+impl Dimension for Latitude {}
+impl Dimension for Longitude {}
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, From)]
+pub struct Latitude;
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, From)]
+pub struct Longitude;
+
+/// A coordinate in a specific dimension T
+///
+/// Coordinates are parameterized with a dimension, so they can't be confused.
+///
+/// For example, coordinates of the same dimension can be compared
+///
+/// ```
+/// use otterhide::new_coordinates::*;
+///
+/// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+/// struct X;
+/// impl Dimension for X {}
+///
+/// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+/// struct Y;
+/// impl Dimension for Y {}
+///
+/// let x1 = Coordinate::<X>::new(-5);
+/// let x2 = Coordinate::<X>::new(20);
+/// assert!(x1 < x2);
+/// ```
+///
+/// But this should not compile with a `mismatched types` error.
+///
+/// ``` compile_fail
+/// # use otterhide::new_coordinates::*;
+/// #
+/// # #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+/// # struct X;
+/// # impl Dimension for X {}
+/// #
+/// # #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+/// # struct Y;
+/// # impl Dimension for Y {}
+/// #
+/// let x = Coordinate::<X>::new(-5);
+/// let y = Coordinate::<Y>::new(-5);
+/// assert_eq!(x, y);
+/// ```
+///
+#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
+pub struct Coordinate<T>(i32, PhantomData<T>)
+where
+ T: Dimension + Clone + Ord + PartialOrd + Eq + PartialEq;
=
=/// First element is longitude from west to east, second is latitude from north to south
=// TODO: Specify Latitude and Longitude types for safety
=#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, AsRef)]
-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);
+pub struct Coordinates(Coordinate<Longitude>, Coordinate<Latitude>);
=
=#[derive(Copy, Clone, PartialEq, Eq)]
=pub enum Direction {
@@ -42,23 +96,23 @@ impl Coordinates {
= self
= }
=
- pub fn longitude(&self) -> Longitude {
+ pub fn longitude(&self) -> Coordinate<Longitude> {
= self.0
= }
=
- pub fn latitude(&self) -> Latitude {
+ pub fn latitude(&self) -> Coordinate<Latitude> {
= self.1
= }
=
- pub fn new(longitude: Longitude, latitude: Latitude) -> Self {
+ pub fn new(longitude: Coordinate<Longitude>, latitude: Coordinate<Latitude>) -> Self {
= Self(longitude, latitude)
= }
=}
=
=impl From<&Coordinates> for Vec2 {
= fn from(coordinates: &Coordinates) -> Self {
- let x: i32 = coordinates.longitude().into();
- let y: i32 = coordinates.latitude().into();
+ let x: i32 = coordinates.longitude().borrow().into();
+ let y: i32 = coordinates.latitude().borrow().into();
=
= Self::new((x * 10) as f32, (y * 10) as f32)
= }
@@ -66,93 +120,149 @@ impl From<&Coordinates> for Vec2 {
=
=impl From<&Coordinates> for Vec3 {
= fn from(coordinates: &Coordinates) -> Self {
- let x: i32 = coordinates.longitude().into();
- let z: i32 = coordinates.latitude().into();
+ let x: i32 = coordinates.longitude().borrow().into();
+ let z: i32 = coordinates.latitude().borrow().into();
=
= Self::new((x * 10) as f32, 0.0, (z * 10) as f32)
= }
=}
=
-// TODO: DRY on Latitude and Longitude. Maybe create a derivable trait Coordinate?
+impl<T: Dimension + Clone + Ord> Coordinate<T> {
+ pub fn new(value: i32) -> Self {
+ Self(value, PhantomData)
+ }
=
-impl Add<i32> for Latitude {
- type Output = Self;
- fn add(self, rhs: i32) -> Self::Output {
- Self(self.0 + rhs)
+ pub fn raw_value(&self) -> i32 {
+ self.0
= }
-}
=
-impl AddAssign<i32> for Latitude {
- fn add_assign(&mut self, rhs: i32) {
- self.0 += rhs;
+ pub fn range(&self, other: &Self) -> Vec<Self> {
+ let start = self.min(other).raw_value();
+ let end = self.max(other).raw_value();
+ let values = start..=end;
+ values.map(Self::new).collect()
= }
-}
=
-impl SubAssign<i32> for Latitude {
- fn sub_assign(&mut self, rhs: i32) {
- self.0 -= rhs;
+ /// Get a distance between two coordinates of the same dimension
+ pub fn distance(&self, other: &Self) -> u32 {
+ self.0.abs_diff(other.0)
= }
=}
=
-impl Add<i32> for Longitude {
+impl<T: Dimension + Clone + Ord> Add<i32> for Coordinate<T> {
= type Output = Self;
= fn add(self, rhs: i32) -> Self::Output {
- Self(self.0 + rhs)
+ Self(self.0 + rhs, PhantomData)
+ }
+}
+
+impl<T: Dimension + Clone + Ord> Sub<i32> for Coordinate<T> {
+ type Output = Self;
+ fn sub(self, rhs: i32) -> Self::Output {
+ Self(self.0 - rhs, PhantomData)
= }
=}
=
-impl AddAssign<i32> for Longitude {
+impl<T: Dimension + Clone + Ord> AddAssign<i32> for Coordinate<T> {
= fn add_assign(&mut self, rhs: i32) {
= self.0 += rhs;
= }
=}
=
-impl SubAssign<i32> for Longitude {
+impl<T: Dimension + Clone + Ord> SubAssign<i32> for Coordinate<T> {
= fn sub_assign(&mut self, rhs: i32) {
= self.0 -= rhs;
= }
=}
=
-impl Latitude {
- pub fn new(value: i32) -> Self {
- Self(value)
+impl<T> From<&Coordinate<T>> for i32
+where
+ T: Dimension + Clone + Ord,
+{
+ fn from(value: &Coordinate<T>) -> Self {
+ value.0
= }
+}
=
- 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(Coordinate(x, ..), Coordinate(z, ..)) = self;
+ write!(f, "({x}, {z})")
= }
+}
=
- /// Get a distance between two latitudes
- pub fn distance(&self, other: &Self) -> u32 {
- self.0.abs_diff(other.0)
+impl<T> Display for Coordinate<T>
+where
+ T: Dimension + Clone + Ord,
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self(value, ..) = self;
+ write!(f, "{value}")
= }
=}
=
-impl Longitude {
- pub fn new(value: i32) -> Self {
- Self(value)
- }
+#[cfg(test)]
+mod coordinate_tests {
+ use super::*;
=
- 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()
- }
+ // In this test we use different dimensions - X and Y
+ #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+ struct X;
+ impl Dimension for X {}
=
- /// Get a distance between two longitudes
- pub fn distance(&self, other: &Self) -> u32 {
- self.0.abs_diff(other.0)
+ #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+ struct Y;
+ impl Dimension for Y {}
+
+ // So the coordinates is also different
+ struct Coordinates {
+ x: Coordinate<X>,
+ y: Coordinate<Y>,
= }
-}
=
-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})")
+ #[test]
+ fn test_coordinate() {
+ let x1 = Coordinate::<X>::new(10);
+ let x2 = Coordinate::<X>::new(-20);
+ let y1 = Coordinate::<Y>::new(10);
+ let y2 = Coordinate::<Y>::new(-20);
+
+ // You can compare coordinates of the same dimension
+ assert!(x1 > x2);
+ assert_ne!(y1, y2);
+ assert_eq!(x1.distance(&x2), 30);
+ assert_eq!(y2.distance(&y1), 30);
+
+ // If you want to compare raw values, you can, but you got to be explicit!
+ assert_eq!(x1.raw_value(), y1.raw_value());
+
+ // A point can be constructed only given correct coordinates
+ // This was a source of some nasty bugs
+ let coordinates = Coordinates { x: x1, y: y1 };
+
+ assert_eq!(coordinates.x, Coordinate::<X>::new(10));
+ assert_eq!(coordinates.y, Coordinate::<Y>::new(10));
+
+ // All of the below should not and will not work. Will produce compile
+ // time errors similar to this:
+ //
+ // mismatched types:
+ // expected struct `new_coordinates::Coordinate<new_coordinates::X>`
+ // found struct `new_coordinates::Coordinate<new_coordinates::Y>`
+ //
+ // ``` compile_fail
+ // assert_eq!(x1, y1);
+ // ```
+ //
+ // ``` compile_fail
+ // let coordinates = Coordinates { x: y2, y: x2 };
+ // ```
+ //
+ // ``` compile_fail
+ // assert_eq!(y2.distance(&x1), 30);
+ // ```
+ //
+ // TODO: Use trybuild to assert compile errors
= }
=}
=
@@ -163,13 +273,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(Coordinate::new(0), Coordinate::new(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(Coordinate::new(4), Coordinate::new(5)));
= }
=}index 05ae3b0..68eeb0a 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,5 +1,5 @@
=use crate::buildings::{building_class, Building};
-use crate::coordinates::{Coordinates, Direction, Latitude, Longitude};
+use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
=use crate::population::Person;
=use crate::roads::{RoadPlan, RoadsSystems};
@@ -263,19 +263,19 @@ impl District {
=
= // Edges
=
- fn south(&self) -> Latitude {
+ fn south(&self) -> Coordinate<Latitude> {
= self.origin.latitude() + (self.width as i32)
= }
=
- fn east(&self) -> Longitude {
+ fn east(&self) -> Coordinate<Longitude> {
= self.origin.longitude() + (self.length as i32)
= }
=
- fn north(&self) -> Latitude {
+ fn north(&self) -> Coordinate<Latitude> {
= self.origin.latitude()
= }
=
- fn west(&self) -> Longitude {
+ fn west(&self) -> Coordinate<Longitude> {
= self.origin.longitude()
= }
=
@@ -406,11 +406,11 @@ fn plan_new_districts(
= info!("Trying to place a district {candidate:?}");
=
= // TODO: Extract into Latitude::same_hemisphere method
- if i32::from(candidate.east()) * i32::from(candidate.west()) < 0 {
+ if i32::from(&candidate.east()) * i32::from(&candidate.west()) < 0 {
= info!("District would cross the latitudinal highway");
= return false;
= }
- if i32::from(candidate.north()) * i32::from(candidate.south()) < 0 {
+ if i32::from(&candidate.north()) * i32::from(&candidate.south()) < 0 {
= info!("District would cross the longitudinal highway");
= return false;
= }
@@ -549,33 +549,45 @@ mod district_tests {
= #[test]
= fn measurements_test() {
= let district = District::new(
- Coordinates::new(Longitude::new(-3), Latitude::new(-2)),
+ Coordinates::new(Coordinate::new(-3), Coordinate::new(-2)),
= 13,
= 7,
= String::default(),
= );
=
- assert_eq!(district.east(), Longitude::new(10));
- assert_eq!(district.west(), Longitude::new(-3));
- assert_eq!(district.north(), Latitude::new(-2));
- assert_eq!(district.south(), Latitude::new(5));
+ assert_eq!(district.east(), Coordinate::new(10));
+ assert_eq!(district.west(), Coordinate::new(-3));
+ assert_eq!(district.north(), Coordinate::new(-2));
+ assert_eq!(district.south(), Coordinate::new(5));
= assert_eq!(district.length(), 13);
= assert_eq!(district.width(), 7);
= assert_eq!(
= district.north_east(),
- Coordinates::new(Longitude::new(10), Latitude::new(-2))
+ Coordinates::new(
+ Coordinate::<Longitude>::new(10),
+ Coordinate::<Latitude>::new(-2)
+ )
= );
= assert_eq!(
= district.north_west(),
- Coordinates::new(Longitude::new(-3), Latitude::new(-2))
+ Coordinates::new(
+ Coordinate::<Longitude>::new(-3),
+ Coordinate::<Latitude>::new(-2)
+ )
= );
= assert_eq!(
= district.south_east(),
- Coordinates::new(Longitude::new(10), Latitude::new(5))
+ Coordinates::new(
+ Coordinate::<Longitude>::new(10),
+ Coordinate::<Latitude>::new(5)
+ )
= );
= assert_eq!(
= district.south_west(),
- Coordinates::new(Longitude::new(-3), Latitude::new(5))
+ Coordinates::new(
+ Coordinate::<Longitude>::new(-3),
+ Coordinate::<Latitude>::new(5)
+ )
= );
= }
=
@@ -587,7 +599,10 @@ mod district_tests {
= assert_eq!(rect.max, Vec2::new(100.0, 200.0));
=
= let district = District::new(
- Coordinates::new(Longitude::new(10), Latitude::new(5)),
+ Coordinates::new(
+ Coordinate::<Longitude>::new(10),
+ Coordinate::<Latitude>::new(5),
+ ),
= 10,
= 20,
= String::default(),
@@ -603,7 +618,10 @@ mod district_tests {
= assert_eq!(district.center(), Vec3::new(50.0, 0.0, 100.0));
=
= let district = District::new(
- Coordinates::new(Longitude::new(10), Latitude::new(5)),
+ Coordinates::new(
+ Coordinate::<Longitude>::new(10),
+ Coordinate::<Latitude>::new(5),
+ ),
= 10,
= 15,
= String::default(),
@@ -630,14 +648,20 @@ mod district_tests {
=
= // Contained
= let a = District {
- origin: Coordinates::new(Longitude::new(-60), Latitude::new(-49)),
+ origin: Coordinates::new(
+ Coordinate::<Longitude>::new(-60),
+ Coordinate::<Latitude>::new(-49),
+ ),
= length: 30,
= width: 49,
= rotation: Rotation::CW270,
= model: "district-490x300-a".to_string(),
= };
= let b = District {
- origin: Coordinates::new(Longitude::new(-60), Latitude::new(-18)),
+ origin: Coordinates::new(
+ Coordinate::<Longitude>::new(-60),
+ Coordinate::<Latitude>::new(-18),
+ ),
= length: 30,
= width: 18,
= rotation: Rotation::CW90,Refactor code in names module
On by
Use type alias and dereference for readability.
index 80a8528..e371c8e 100644
--- a/src/names.rs
+++ b/src/names.rs
@@ -3,9 +3,9 @@ use rand::{seq::SliceRandom, thread_rng};
=use crate::population::Sex;
=
=pub fn random_first_name(sex: &Sex) -> String {
- let names = match sex {
- &Sex::Male => MALE_NAMES,
- &Sex::Female => FEMALE_NAMES,
+ let names = match *sex {
+ Sex::Male => MALE_NAMES,
+ Sex::Female => FEMALE_NAMES,
= };
=
= names.choose(&mut thread_rng()).unwrap().to_string()
@@ -184,7 +184,7 @@ const LAST_NAMES: NamesList = &[
= "Zebra",
=];
=
-const FEMALE_NAMES: &'static [&'static str] = &[
+const FEMALE_NAMES: NamesList = &[
= "Aaliyah",
= "Abigail",
= "Adriana",Test and prevent district_size from panicking
On by
Instead of panicking, it will return Option::None.
index 68eeb0a..327a155 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -57,14 +57,42 @@ pub fn district_size(name: &str) -> Option<(u32, u32)> {
= let district_name_pattern =
= Regex::new(r"^district-(?<length>\d+)x(?<width>\d+)-(?<variant>.+)$").unwrap();
= district_name_pattern.captures(name).and_then(|captured| {
- let length: u32 = captured["length"].parse().unwrap();
- let width: u32 = captured["width"].parse().unwrap();
+ let length: u32 = captured["length"].parse().ok()?;
+ let width: u32 = captured["width"].parse().ok()?;
= // TODO: Consider renaming scenes in blender to use 10m units
= // i.e. instead of district-180x300 call it district-18x30
= Some((length / 10, width / 10))
= })
=}
=
+#[cfg(test)]
+mod district_size_tests {
+ use super::*;
+
+ #[test]
+ fn valid_name() {
+ let name = "district-180x300-a";
+ let expected = Some((18, 30));
+ let actual = district_size(name);
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn invalid_name() {
+ // Negatives - bad
+ let name = "district-10x-20-qux";
+ let expected = None;
+ let actual = district_size(name);
+ assert_eq!(actual, expected);
+
+ // Not a number - bad
+ let name = "district-barxbaz-qux";
+ let expected = None;
+ let actual = district_size(name);
+ assert_eq!(actual, expected);
+ }
+}
+
=fn setup_districts(
= districts_assets: Res<DistrictAssets>,
= assets: Res<Assets<Gltf>>,Don't clone a date - it implements the Copy trait
On by
index 16132f0..15c75d3 100644
--- a/src/date.rs
+++ b/src/date.rs
@@ -179,7 +179,7 @@ pub fn past_hour(hour: f32) -> impl FnMut(Option<Res<Date>>, Local<Option<DayMon
= // On first run, pretend that it happened on previous daymonth
= let previous = last_ran.unwrap_or(*date.0.clone().set_hour(hour).advance(-1.0));
= let next = *previous.clone().advance(1.0);
- let now = date.0.clone();
+ let now = date.0;
=
= if now >= next {
= // Do it, and pretend it happened on timeLet the check goal run tests and then clippy
On by
Running make check will watch for code changes, run unit tests, and if
they pass, run the linter (Clippy).
index afd9576..7a302c8 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ serve: web
=
=check: ## Check and recheck the code after every change
=check:
- watchexec --watch src/ --debounce 1s cargo clippy
+ watchexec --watch src/ --debounce 1s 'cargo test && cargo clippy'
=.PHONY: check
=
=help: ## Print this help messageImprove building module naming convention
On by
Some of the terms we used in code related to building classes, models, etc. was confusing. There are few new concepts and related types:
-
BuildingVariant
Consists of a class (string) and appearance (number). It's derived from a scene name in buildings.blend, for example:
building-medium.003 -> BuildingVariant { class: "medium", appearance: 3, }It implements FromStr trait, so a name can be parsed into a BuildingVariant.
-
Use the word "scene" instead of "model" when referring to a Handle
For now only in context of buildings. There is similar confusion in the districts code, but this commit only deals with buildings related code.
index d927523..c846829 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -9,17 +9,19 @@ use bevy::utils::HashMap;
=use itertools::Itertools;
=use rand::seq::IteratorRandom;
=use regex::Regex;
+use std::fmt::Display;
+use std::str::FromStr;
=
=pub struct BuildingsPlugin;
=
=impl Plugin for BuildingsPlugin {
= fn build(&self, app: &mut App) {
= app.add_event::<ConstructionOrder>()
- .init_resource::<BuildingModels>()
- .register_type::<BuildingModels>()
+ .init_resource::<BuildingScenes>()
+ .register_type::<BuildingScenes>()
= .add_systems(Startup, setup_assets)
= .add_systems(Startup, register_buildings_systems)
- .add_systems(OnEnter(GameState::Simulate), setup_building_models)
+ .add_systems(OnEnter(GameState::Simulate), setup_building_scenes)
= .add_systems(
= Update,
= order_construction.run_if(
@@ -67,62 +69,55 @@ fn setup_assets(
=
=#[derive(Resource, Default, Debug, Reflect)]
=#[reflect(Resource)]
-pub struct BuildingModels(pub HashMap<String, BuildingModel>);
+pub struct BuildingScenes(pub HashMap<BuildingVariant, Handle<Scene>>);
=
-#[derive(Debug, Reflect)]
-pub struct BuildingModel {
- pub class: String,
- pub scene: Handle<Scene>,
-}
-
-impl BuildingModels {
+impl BuildingScenes {
= pub fn classes(&self) -> Vec<String> {
= self.0
- .values()
- .map(|BuildingModel { class, .. }| class)
+ .keys()
+ .map(|BuildingVariant { class, .. }| class)
= .unique()
= .cloned()
= .collect()
= }
=
- pub fn get_class(&self, class: &str) -> HashMap<String, Handle<Scene>> {
- let mut models = HashMap::default();
- for (name, model) in self.0.iter() {
- if model.class == class {
- models.insert(name.to_string(), model.scene.clone());
- }
- }
- models
+ pub fn get_class(&self, class: &str) -> HashMap<BuildingVariant, Handle<Scene>> {
+ self.0
+ .clone()
+ .iter()
+ .filter_map(|(variant, scene)| {
+ if variant.class == class {
+ Some((variant.clone(), scene.clone()))
+ } else {
+ None
+ }
+ })
+ .collect()
= }
=}
=
-pub fn building_class(name: &str) -> Option<String> {
- let building_class_pattern = Regex::new(r"^building-(?<class>.+)\.(?<variant>\d{3})$").unwrap();
- building_class_pattern
- .captures(name)
- .map(|captured| captured["class"].to_string())
-}
-
-fn setup_building_models(
+fn setup_building_scenes(
= building_assets: Res<BuildingAssets>,
= assets: Res<Assets<Gltf>>,
- mut models: ResMut<BuildingModels>,
+ mut scenes: ResMut<BuildingScenes>,
=) {
= let buildings = assets.get(&building_assets.0).unwrap();
= for (name, scene) in &buildings.named_scenes {
= info!("Looking for buildings in scene {name}");
- if let Some(class) = building_class(name) {
- info!("Registering {name} in class {class}");
-
- models.0.insert(
- name.to_string(),
- BuildingModel {
- class,
- scene: scene.clone(),
- },
- );
- }
+ let Ok(variant) = name.parse::<BuildingVariant>() else {
+ warn!("Scene name {name} cannot be parsed as a building variant");
+ continue;
+ };
+
+ scenes.0.insert(variant, scene.clone());
= }
+
+ let classes = scenes
+ .classes()
+ .iter()
+ .map(|class| format!(" - {class}"))
+ .join("\n");
+ info!("Building classes registered:\n{classes}");
=}
=
=/// This event represents a decision to construct a new building.
@@ -134,13 +129,49 @@ pub struct ConstructionOrder(pub BuildingDescription);
=#[derive(Clone, Debug)]
=pub struct BuildingDescription {
= pub transform: Transform,
- pub model: String,
+ pub variant: BuildingVariant,
=}
=
=/// A tag for building entities
=#[derive(Component)]
=pub struct Building {
- model: String,
+ variant: BuildingVariant,
+}
+
+
+/// A variant identifies the building scene
+///
+/// Variants come from the buildings.blend file (and buildings.glb exported from
+/// Blender). They are parsed from scene names.
+#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect)]
+pub struct BuildingVariant {
+ pub class: String,
+ pub appearance: u8,
+}
+
+impl Display for BuildingVariant {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self { class, appearance } = self;
+ write!(f, "{class}.{appearance}")
+ }
+}
+
+impl FromStr for BuildingVariant {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ // TODO: Make invalid regexp a compile time error
+ let pattern = Regex::new(r"^building-(?<class>.+)\.(?<appearance>\d{3})$").unwrap();
+
+ pattern
+ .captures(s)
+ .and_then(|captured| {
+ let class = captured["class"].to_string();
+ let appearance = captured["appearance"].parse().ok()?;
+ Self { class, appearance }.into()
+ })
+ .ok_or("Can't parse '{s}' as a building variant.".to_string())
+ }
=}
=
=/// Issue a construction order, for the record and effect
@@ -150,7 +181,7 @@ fn order_construction(
= parcels: Query<(Entity, &Parcel, &GlobalTransform)>,
= buildings: Query<&Building>,
= mut commands: Commands,
- models: Res<BuildingModels>,
+ scenes: Res<BuildingScenes>,
=) {
= let count = parcels.iter().count();
= info!("There are {count} parcels now.");
@@ -160,8 +191,8 @@ fn order_construction(
= let housing_shortage = population.saturating_sub(capacity);
=
= for (entity, parcel, transform) in parcels.iter().take(housing_shortage) {
- let models = &models.get_class(&parcel.class);
- let model = models.keys().choose(&mut rand::thread_rng()).unwrap();
+ let scenes = &scenes.get_class(&parcel.class);
+ let variant = scenes.keys().choose(&mut rand::thread_rng()).unwrap();
=
= // Remove parcel
= commands.entity(entity).despawn();
@@ -169,7 +200,7 @@ fn order_construction(
= // Order the construction
= let description = BuildingDescription {
= transform: transform.to_owned().into(),
- model: model.to_string(),
+ variant: variant.clone(),
= };
= build_events.send(ConstructionOrder(description));
= }
@@ -190,23 +221,24 @@ fn receive_orders(
=fn construct_building(
= In(description): In<BuildingDescription>,
= mut commands: Commands,
- models: Res<BuildingModels>,
+ scenes: Res<BuildingScenes>,
=) {
- let Vec3 { x, z, .. } = description.transform.translation;
- let model = models.0.get(&description.model).unwrap();
+ let BuildingDescription { variant, transform } = description.clone();
+ let Vec3 { x, z, .. } = transform.translation;
+ let scene = scenes.0.get(&variant).unwrap().clone();
+ let name = format!("Building {variant}");
=
- info!("Spawning a new building at ({x:.2}, {z:.2})!");
+ info!("Spawning a new {variant} at ({x:.2}, {z:.2})!");
=
+ // TODO: Use a BuildingBundle
= commands
= .spawn(SceneBundle {
- scene: model.scene.clone(),
- transform: description.transform,
+ scene,
+ transform,
= ..default()
= })
- .insert(Building {
- model: description.model.clone(),
- })
- .insert(Name::new(description.model));
+ .insert(Building { variant })
+ .insert(Name::new(name));
=}
=
=pub type Snapshot = Vec<BuildingDescription>;
@@ -214,8 +246,8 @@ pub type Snapshot = Vec<BuildingDescription>;
=pub fn take_snapshot(buildings: Query<(&Building, &Transform)>) -> Snapshot {
= buildings
= .iter()
- .map(|(Building { model }, transform)| BuildingDescription {
- model: model.to_string(),
+ .map(|(Building { variant }, transform)| BuildingDescription {
+ variant: variant.clone(),
= transform: *transform,
= })
= .collect()index 327a155..470015f 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -1,4 +1,4 @@
-use crate::buildings::{building_class, Building};
+use crate::buildings::{Building, BuildingVariant};
=use crate::coordinates::{Coordinate, Coordinates, Direction, Latitude, Longitude};
=use crate::date::SimulationFrameCounter;
=use crate::population::Person;
@@ -128,7 +128,8 @@ fn setup_districts(
= return None;
= };
= // Check if the name matches pattern
- building_class(name).map(|class| (entity, class, *transform))
+ let BuildingVariant { class, .. } = name.parse().ok()?;
+ (entity, class, *transform).into()
= })
= .collect_vec();
=index 55e3859..a34dfb4 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -63,12 +63,12 @@ impl Display for HistoricalEvent {
= match self {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
= transform,
- model,
+ variant,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
= write!(
= f,
- "construction of a new building ({model}) ordered at ({x:.2}, {z:.2})",
+ "construction of a new building ({variant}) ordered at ({x:.2}, {z:.2})",
= )
= }
= HistoricalEvent::NewDistrictEstablished(NewDistrictEstablished(district)) => {index c705676..2ce7b26 100644
--- a/src/pgsql_export.rs
+++ b/src/pgsql_export.rs
@@ -34,20 +34,20 @@ impl Display for History {
= match event.event.clone() {
= HistoricalEvent::ConstructionOrder(ConstructionOrder(BuildingDescription {
= transform,
- model,
+ variant,
= })) => {
= let Vec3 { x, z, .. } = transform.translation;
=
= writeln!(f, "Insert into buildings (")?;
= writeln!(f, " date,")?;
- writeln!(f, " model,")?;
+ writeln!(f, " variant,")?;
= writeln!(f, " coordinates")?;
= writeln!(f, ") values (")?;
= writeln!(
= f,
= " {year}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
= )?;
- writeln!(f, " {model}")?;
+ writeln!(f, " {variant}")?;
= writeln!(f, " ({x:.3}, {z:.3})")?;
= writeln!(f, ");")?;
= }Use wider number for buildings appearance
On by
Otherwise parsing variant to extract a parcel class within a district with a large number of parcels will fail and some sample houses are not replaced with parcels.
index c846829..9d8126e 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -146,7 +146,7 @@ pub struct Building {
=#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect)]
=pub struct BuildingVariant {
= pub class: String,
- pub appearance: u8,
+ pub appearance: u16,
=}
=
=impl Display for BuildingVariant {Use a component bundle to spawn buildings
On by
Using a bundle of components increases readability and type-safety.
The new bundle is called BuildingBundle.
index 9d8126e..a4adc19 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -134,10 +134,32 @@ pub struct BuildingDescription {
=
=/// A tag for building entities
=#[derive(Component)]
-pub struct Building {
+pub struct Building;
+
+#[derive(Bundle)]
+pub struct BuildingBundle {
= variant: BuildingVariant,
+ scene: SceneBundle,
+ name: Name,
+ marker: Building,
=}
=
+impl BuildingBundle {
+ pub fn new(variant: BuildingVariant, transform: Transform, scenes: &BuildingScenes) -> Self {
+ let scene = scenes.0.get(&variant).unwrap().clone();
+ let name = format!("Building {variant}").into();
+ Self {
+ variant,
+ name,
+ scene: SceneBundle {
+ scene,
+ transform,
+ ..default()
+ },
+ marker: Building,
+ }
+ }
+}
=
=/// A variant identifies the building scene
=///
@@ -225,28 +247,17 @@ fn construct_building(
=) {
= let BuildingDescription { variant, transform } = description.clone();
= let Vec3 { x, z, .. } = transform.translation;
- let scene = scenes.0.get(&variant).unwrap().clone();
- let name = format!("Building {variant}");
=
- info!("Spawning a new {variant} at ({x:.2}, {z:.2})!");
-
- // TODO: Use a BuildingBundle
- commands
- .spawn(SceneBundle {
- scene,
- transform,
- ..default()
- })
- .insert(Building { variant })
- .insert(Name::new(name));
+ debug!("Spawning a new {variant} building at ({x:.2}, {z:.2})!");
+ commands.spawn(BuildingBundle::new(variant, transform, &scenes));
=}
=
=pub type Snapshot = Vec<BuildingDescription>;
=
-pub fn take_snapshot(buildings: Query<(&Building, &Transform)>) -> Snapshot {
+pub fn take_snapshot(buildings: Query<(&BuildingVariant, &Transform), With<Building>>) -> Snapshot {
= buildings
= .iter()
- .map(|(Building { variant }, transform)| BuildingDescription {
+ .map(|(variant, transform)| BuildingDescription {
= variant: variant.clone(),
= transform: *transform,
= })Rename BuildingVariant appearance field to number
On by
It's more generic and implies that there is no semantic meaning to this field. It's just a differentiation, as in Channel N° 5.
index a4adc19..334720c 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -168,13 +168,19 @@ impl BuildingBundle {
=#[derive(Component, Debug, Clone, Hash, PartialEq, Eq, Reflect)]
=pub struct BuildingVariant {
= pub class: String,
- pub appearance: u16,
+
+ /// The numerical value to distinguish between variants withing the same class
+ ///
+ /// The .001, .002, etc. part in Blender's scene name, for example 2 in
+ /// «building-house-small.002». It doesn't have any semantic meaning, other
+ /// than to distinguish between otherwise equivalent building variants.
+ pub number: u16,
=}
=
=impl Display for BuildingVariant {
= fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Self { class, appearance } = self;
- write!(f, "{class}.{appearance}")
+ let Self { class, number } = self;
+ write!(f, "{class}.{number}")
= }
=}
=
@@ -189,8 +195,8 @@ impl FromStr for BuildingVariant {
= .captures(s)
= .and_then(|captured| {
= let class = captured["class"].to_string();
- let appearance = captured["appearance"].parse().ok()?;
- Self { class, appearance }.into()
+ let number = captured["appearance"].parse().ok()?;
+ Self { class, number }.into()
= })
= .ok_or("Can't parse '{s}' as a building variant.".to_string())
= }Refactor districts module
On by
-
Introduce DistrictVariant type
It's value can be parsed from scene names in .blend (.glb) files.
The scenes need to have .001, .002 etc. suffix instead of old -a, -b, etc.
-
Simplify the rotation logic
-
Clarify terminology around scenes, handles, descriptions, etc.
-
Use bundle for spawning new districts
-
Contain complicated logic around transform in the DistrictDescription
index f5536e1..96a0c9f 100644
Binary files a/art/districts.blend and b/art/districts.blend differindex 70d762f..0c362f1 100644
Binary files a/assets/districts.glb and b/assets/districts.glb differindex 470015f..e820bb0 100644
--- a/src/districts.rs
+++ b/src/districts.rs
@@ -16,6 +16,7 @@ use std::borrow::Borrow;
=use std::f32::consts::FRAC_PI_2;
=use std::iter;
=use std::ops::{AddAssign, Not};
+use std::str::FromStr;
=
=pub struct DistrictsPlugin;
=
@@ -23,7 +24,7 @@ impl Plugin for DistrictsPlugin {
= fn build(&self, app: &mut App) {
= app.add_event::<NewDistrictEstablished>()
= .register_type::<Parcel>()
- .init_resource::<DistrictModels>()
+ .init_resource::<DistrictScenes>()
= .add_systems(Startup, register_district_systems)
= .add_systems(Startup, setup_assets)
= .add_systems(OnEnter(GameState::Simulate), setup_districts)
@@ -38,8 +39,9 @@ impl Plugin for DistrictsPlugin {
= }
=}
=
+/// Remembers where to find the districts GLTF asset among all the other
=#[derive(Resource)]
-struct DistrictAssets(Handle<Gltf>);
+struct DistrictsAssetHandle(Handle<Gltf>);
=
=fn setup_assets(
= assets: Res<AssetServer>,
@@ -50,30 +52,22 @@ fn setup_assets(
= info!("Loading {ASSET_PATH} asset");
= let handle = assets.load(ASSET_PATH);
= preloaded.0.insert(handle.clone().untyped());
- commands.insert_resource(DistrictAssets(handle));
-}
-
-pub fn district_size(name: &str) -> Option<(u32, u32)> {
- let district_name_pattern =
- Regex::new(r"^district-(?<length>\d+)x(?<width>\d+)-(?<variant>.+)$").unwrap();
- district_name_pattern.captures(name).and_then(|captured| {
- let length: u32 = captured["length"].parse().ok()?;
- let width: u32 = captured["width"].parse().ok()?;
- // TODO: Consider renaming scenes in blender to use 10m units
- // i.e. instead of district-180x300 call it district-18x30
- Some((length / 10, width / 10))
- })
+ commands.insert_resource(DistrictsAssetHandle(handle));
=}
=
=#[cfg(test)]
-mod district_size_tests {
+mod district_variant_tests {
= use super::*;
=
= #[test]
= fn valid_name() {
- let name = "district-180x300-a";
- let expected = Some((18, 30));
- let actual = district_size(name);
+ let name = "district-180x300.002";
+ let expected = Ok(DistrictVariant {
+ length: 18,
+ width: 30,
+ number: 2,
+ });
+ let actual = name.parse();
= assert_eq!(actual, expected);
= }
=
@@ -81,34 +75,43 @@ mod district_size_tests {
= fn invalid_name() {
= // Negatives - bad
= let name = "district-10x-20-qux";
- let expected = None;
- let actual = district_size(name);
+ let expected: Result<DistrictVariant, _> =
+ Err(DistrictVariantError::NotMatchingPattern(name.to_string()));
+ let actual = name.parse();
= assert_eq!(actual, expected);
=
= // Not a number - bad
= let name = "district-barxbaz-qux";
- let expected = None;
- let actual = district_size(name);
+ let expected: Result<DistrictVariant, _> =
+ Err(DistrictVariantError::NotMatchingPattern(name.to_string()));
+ let actual = name.parse();
+ assert_eq!(actual, expected);
+
+ // Not divisible by 10 - bad
+ let name = "district-200x205-qux";
+ let expected: Result<DistrictVariant, _> =
+ Err(DistrictVariantError::NotMatchingPattern(name.to_string()));
+ let actual = name.parse();
= assert_eq!(actual, expected);
= }
=}
=
=fn setup_districts(
- districts_assets: Res<DistrictAssets>,
- assets: Res<Assets<Gltf>>,
- mut scenes: ResMut<Assets<Scene>>,
- mut descriptions: ResMut<DistrictModels>,
+ districts_asset_handle: Res<DistrictsAssetHandle>,
+ gltf_assets: Res<Assets<Gltf>>,
+ mut scenes_assets: ResMut<Assets<Scene>>,
+ mut scenes: ResMut<DistrictScenes>,
=) {
- let districts = assets.get(&districts_assets.0).unwrap();
+ let districts_gltf = gltf_assets.get(&districts_asset_handle.0).unwrap();
=
= // TODO: Do it for every scene
- for (name, scene_handle) in districts.named_scenes.iter() {
+ for (name, scene_handle) in districts_gltf.named_scenes.iter() {
= info!("Processing district {name}");
- let Some((length, width)) = district_size(name) else {
- panic!("The size could not be determined from the district's name: {name}");
+ let Ok(variant) = name.parse::<DistrictVariant>() else {
+ panic!("The districts asset contains a scene that can't be parsed into a DistrictVariant: {name}");
= };
=
- let scene = scenes.get_mut(scene_handle.clone()).unwrap();
+ let scene = scenes_assets.get_mut(scene_handle.clone()).unwrap();
=
= let root = scene
= .world
@@ -151,22 +154,123 @@ fn setup_districts(
= };
= }
=
- let description = DistrictModel { width, length };
-
- descriptions.0.insert(name.to_string(), description);
+ scenes.0.insert(variant, scene_handle.clone());
= }
=
- info!("Districts processed: {descriptions:?}");
+ let variants = scenes
+ .0
+ .keys()
+ .map(|variant| format!(" - {variant:?}"))
+ .join("\n");
+ info!("Districts processed: \n{variants}");
=}
=
-#[derive(Debug)]
-struct DistrictModel {
- width: u32,
+#[derive(Bundle)]
+struct DistrictBundle {
+ marker: District,
+ /// The logical description of the district, used for snapshots
+ description: DistrictDescription,
+ /// Do not set the scene directly. it is derived from the description.
+ scene: SceneBundle,
+ name: Name,
+}
+
+impl DistrictBundle {
+ fn new(description: DistrictDescription, scenes: &DistrictScenes) -> Self {
+ let scene = SceneBundle {
+ scene: scenes.0.get(&description.variant).unwrap().clone(),
+ transform: description.borrow().into(),
+ ..default()
+ };
+ Self {
+ description,
+ scene,
+ name: Name::new("District"),
+ marker: District,
+ }
+ }
+}
+
+/// A marker for district entities
+
+#[derive(Component, Debug, Clone, Copy)]
+pub struct District;
+
+#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct DistrictVariant {
+ /// The length of the district model
+ ///
+ /// Be careful when reading this value in a context of a district
+ /// description, as it's rotation may swap original length and width. Better
+ /// use the DistrictDescription::length method.
= length: u32,
+
+ /// The width of the district model
+ ///
+ /// Be careful when reading this value in a context of a district
+ /// description, as it's rotation may swap original length and width. Better
+ /// use the DistrictDescription::width method.
+ width: u32,
+
+ /// The numerical value to distinguish between variants with the same size
+ ///
+ /// The .001, .002, etc. part in Blender's scene name. For example it will
+ /// be 2 in «district-180x300.002». It doesn't have any semantic meaning,
+ /// other than to distinguish between otherwise equivalent building
+ /// variants.
+ number: u16,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DistrictVariantError {
+ NotMatchingPattern(String),
+ InvalidLength(String),
+ InvalidWidth(String),
+ InvalidNumber(String),
+}
+
+impl FromStr for DistrictVariant {
+ type Err = DistrictVariantError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ // If pattern is invalid, it's not recoverable.
+ // TODO: Make invalid pattern a compilation error.
+ // NOTE: Pattern assumes length and width end in 0
+ // They must be divisible by 10 meters. Only the preceding digits are
+ // captured, effectively dividing the input by 10, so it matches units
+ // of coordinates. It's a hack. Ugly?
+ let pattern =
+ Regex::new(r"^district-(?<length>\d+)0x(?<width>\d+)0\.(?<number>\d+)$").unwrap();
+ let Some(captures) = pattern.captures(s) else {
+ return Err(DistrictVariantError::NotMatchingPattern(s.to_string()));
+ };
+
+ // Extract captured groups
+ let length = captures["length"].to_string();
+ let width = captures["width"].to_string();
+ let number = captures["number"].to_string();
+
+ // Parse
+ let Ok(length) = length.parse::<u32>() else {
+ return Err(DistrictVariantError::InvalidLength(length));
+ };
+ let Ok(width) = width.parse::<u32>() else {
+ return Err(DistrictVariantError::InvalidWidth(width));
+ };
+ let Ok(number) = number.parse::<u16>() else {
+ return Err(DistrictVariantError::InvalidNumber(number));
+ };
+
+ Ok(DistrictVariant {
+ length,
+ width,
+ number,
+ })
+ }
=}
=
=#[derive(Resource, Debug, Default)]
-struct DistrictModels(HashMap<String, DistrictModel>);
+struct DistrictScenes(HashMap<DistrictVariant, Handle<Scene>>);
=
=#[derive(Component, Reflect, Debug)]
=#[reflect(Component)]
@@ -178,22 +282,20 @@ pub struct Parcel {
=///
=/// It will be stored in the history, and will result in spawning a new building.
=#[derive(Event, Clone, Debug)]
-pub struct NewDistrictEstablished(pub District);
+pub struct NewDistrictEstablished(pub DistrictDescription);
=
=// TODO: Divide into smaller components.
-/// A tag for district entities
+/// A logical description of the district, used for planning and snapshotting
=#[derive(Component, Debug, Clone)]
-pub struct District {
- /// Coordinates of the north-west corner
+pub struct DistrictDescription {
+ /// Coordinates of the north-west corner, after the rotation
= pub origin: Coordinates,
- /// The size longitudinal (x-axis aligned) dimension
- pub length: u32,
- /// The size latitudinal (z-axis aligned) dimension
- pub width: u32,
+
= /// How is the district rotated compared to the model
- rotation: Rotation,
+ pub rotation: Rotation,
+
= /// Which model to use
- model: String,
+ pub variant: DistrictVariant,
=}
=
=#[derive(Component, Debug, Clone, Copy)]
@@ -236,13 +338,11 @@ impl From<isize> for Rotation {
= }
=}
=
-impl District {
- pub fn new(origin: Coordinates, length: u32, width: u32, model: String) -> Self {
+impl DistrictDescription {
+ pub fn new(origin: Coordinates, variant: DistrictVariant) -> Self {
= Self {
= origin,
- length,
- width,
- model,
+ variant,
= rotation: Rotation::None,
= }
= }
@@ -252,22 +352,8 @@ impl District {
= self
= }
=
+ // TODO: Consider removing District::rotate, since it's just an add_assign on rotation
= pub fn rotate(&mut self, rotation: Rotation) -> &mut Self {
- let Self { length, width, .. } = *self;
-
- match rotation {
- Rotation::None => {}
- Rotation::CW90 => {
- self.length = width;
- self.width = length;
- }
- Rotation::CW180 => {}
- Rotation::CW270 => {
- self.length = width;
- self.width = length;
- }
- }
-
= self.rotation += rotation;
= self
= }
@@ -293,11 +379,11 @@ impl District {
= // Edges
=
= fn south(&self) -> Coordinate<Latitude> {
- self.origin.latitude() + (self.width as i32)
+ self.origin.latitude() + (self.width() as i32)
= }
=
= fn east(&self) -> Coordinate<Longitude> {
- self.origin.longitude() + (self.length as i32)
+ self.origin.longitude() + (self.length() as i32)
= }
=
= fn north(&self) -> Coordinate<Latitude> {
@@ -323,7 +409,7 @@ impl District {
= *self
= .origin
= .clone()
- .shift(self.length as i32, &Direction::East)
+ .shift(self.length() as i32, &Direction::East)
= }
=
= fn north_west(&self) -> Coordinates {
@@ -334,32 +420,44 @@ impl District {
= *self
= .origin
= .clone()
- .shift(self.width as i32, &Direction::South)
- .shift(self.length as i32, &Direction::East)
+ .shift(self.width() as i32, &Direction::South)
+ .shift(self.length() as i32, &Direction::East)
= }
=
= fn south_west(&self) -> Coordinates {
= *self
= .origin
= .clone()
- .shift(self.width as i32, &Direction::South)
+ .shift(self.width() as i32, &Direction::South)
= }
=
= // Dimensions
=
= /// Dimension from the east to the west edge (along the longitude)
+ ///
+ /// Depends on rotation and variant.
= fn length(&self) -> u32 {
- self.east().distance(&self.west())
+ match self.rotation {
+ Rotation::None => self.variant.length,
+ Rotation::CW90 => self.variant.width,
+ Rotation::CW180 => self.variant.length,
+ Rotation::CW270 => self.variant.width,
+ }
= }
=
= /// Dimension from the north to the south edge (along the latitude)
= fn width(&self) -> u32 {
- self.north().distance(&self.south())
+ match self.rotation {
+ Rotation::None => self.variant.width,
+ Rotation::CW90 => self.variant.length,
+ Rotation::CW180 => self.variant.width,
+ Rotation::CW270 => self.variant.length,
+ }
= }
=}
=
-impl From<&District> for Rect {
- fn from(value: &District) -> Self {
+impl From<&DistrictDescription> for Rect {
+ fn from(value: &DistrictDescription) -> Self {
= Rect::from_corners(
= value.north_west().borrow().into(),
= value.south_east().borrow().into(),
@@ -367,13 +465,37 @@ impl From<&District> for Rect {
= }
=}
=
+impl From<&DistrictDescription> for Transform {
+ fn from(description: &DistrictDescription) -> Self {
+ let alignment: Transform = match description.rotation {
+ Rotation::None => Transform::default(),
+ Rotation::CW90 => Transform::from_translation(Vec3 {
+ z: (description.length() * 10) as f32 * -1.0,
+ ..default()
+ }),
+ Rotation::CW180 => Transform::from_translation(Vec3 {
+ x: (description.length() * 10) as f32 * -1.0,
+ z: (description.width() * 10) as f32 * -1.0,
+ ..default()
+ }),
+ Rotation::CW270 => Transform::from_translation(Vec3 {
+ x: (description.width() * 10) as f32 * -1.0,
+ ..default()
+ }),
+ };
+ Transform::from_translation(description.origin.borrow().into())
+ .with_rotation(description.rotation.into())
+ .mul_transform(alignment)
+ }
+}
+
=fn plan_new_districts(
- districts: Query<&District>,
+ districts: Query<&DistrictDescription>,
= settings: Res<SimulationParameters>,
= people: Query<&Person>,
= parcels: Query<&Parcel>,
= buildings: Query<&Building>,
- descriptions: Res<DistrictModels>,
+ scenes: Res<DistrictScenes>,
= mut new_districts: EventWriter<NewDistrictEstablished>,
=) {
= let population = people.iter().count();
@@ -385,7 +507,7 @@ fn plan_new_districts(
= info!("Not enough living space ({population} / {capacity}). Planning a new district.");
=
= // TODO: Use some smarter heuristics to choose the district
- let (model, description) = descriptions.0.iter().choose(&mut thread_rng()).unwrap();
+ let variant = scenes.0.keys().choose(&mut thread_rng()).unwrap();
=
= let corners = districts
= .iter()
@@ -394,14 +516,7 @@ fn plan_new_districts(
= .unique();
=
= let candidates = corners
- .map(|corner| {
- District::new(
- corner,
- description.length,
- description.width,
- model.to_string(),
- )
- })
+ .map(|corner| DistrictDescription::new(corner, variant.clone()))
= .flat_map(|candidate| {
= [
= candidate.clone().rotate(Rotation::None).to_owned(),
@@ -412,7 +527,8 @@ fn plan_new_districts(
= .into_iter()
= })
= .flat_map(|candidate| {
- let District { length, width, .. } = candidate;
+ let DistrictDescription { variant, .. } = candidate.clone();
+ let DistrictVariant { length, width, .. } = variant;
= [
= candidate.clone(),
= candidate
@@ -444,7 +560,7 @@ fn plan_new_districts(
= return false;
= }
=
- let outer_buffer = ((candidate.length + candidate.width) * 10) as f32;
+ let outer_buffer = ((candidate.length() + candidate.width()) * 10) as f32;
= let max_distance = settings.land_radius - outer_buffer;
= if candidate.center().length() > max_distance {
= info!("District would be too close to the edge.");
@@ -478,15 +594,15 @@ fn implement_new_districts(
= }
=}
=
-pub type Snapshot = Vec<District>;
+pub type Snapshot = Vec<DistrictDescription>;
=
-pub fn take_snapshot(districts: Query<&District>) -> Snapshot {
+pub fn take_snapshot(districts: Query<&DistrictDescription>) -> Snapshot {
= districts.iter().cloned().collect()
=}
=
=fn rollback(
= In(snapshot): In<Snapshot>,
- districts: Query<Entity, With<District>>,
+ districts: Query<Entity, With<DistrictDescription>>,
= mut commands: Commands,
= systems: Res<DistrictsSystems>,
=) {
@@ -505,7 +621,7 @@ fn rollback(
=pub struct DistrictsSystems {
= pub take_snapshot: SystemId<(), Snapshot>,
= pub rollback: SystemId<Snapshot>,
- pub construct_district: SystemId<District>,
+ pub construct_district: SystemId<DistrictDescription>,
=}
=
=fn register_district_systems(world: &mut World) {
@@ -521,52 +637,18 @@ fn register_district_systems(world: &mut World) {
=}
=
=fn construct_district(
- In(district): In<District>,
- districts_assets: Res<DistrictAssets>,
- assets: Res<Assets<Gltf>>,
+ In(description): In<DistrictDescription>,
+ scenes: Res<DistrictScenes>,
+ roads_systems: Res<RoadsSystems>,
= mut commands: Commands,
- road_systems: Res<RoadsSystems>,
=) {
- info!("Spawning a district: {district:?}");
-
- let districts = assets.get(&districts_assets.0).unwrap();
- let scene_handle = districts.named_scenes.get(&district.model).unwrap();
-
- // Rotate and translate the district model
- // TODO: Move the transform logic below to a District::apply_transform method or something like that
- let alignment: Transform = match district.rotation {
- Rotation::None => Transform::default(),
- Rotation::CW90 => Transform::from_translation(Vec3 {
- z: (district.length * 10) as f32 * -1.0,
- ..default()
- }),
- Rotation::CW180 => Transform::from_translation(Vec3 {
- x: (district.length * 10) as f32 * -1.0,
- z: (district.width * 10) as f32 * -1.0,
- ..default()
- }),
- Rotation::CW270 => Transform::from_translation(Vec3 {
- x: (district.width * 10) as f32 * -1.0,
- ..default()
- }),
- };
- let transform = Transform::from_translation(district.origin.borrow().into())
- .with_rotation(district.rotation.into())
- .mul_transform(alignment);
-
- commands
- .spawn(SceneBundle {
- scene: scene_handle.clone(),
- transform,
- ..default()
- })
- .insert(district.clone())
- .insert(Name::new("District"));
+ info!("Spawning a district: {description:?}");
=
= commands.run_system_with_input(
- road_systems.implement_road_plan,
- district.plan_border_roads(),
+ roads_systems.implement_road_plan,
+ description.plan_border_roads(),
= );
+ commands.spawn(DistrictBundle::new(description, &scenes));
=}
=
=#[cfg(test)]
@@ -577,11 +659,13 @@ mod district_tests {
=
= #[test]
= fn measurements_test() {
- let district = District::new(
+ let district = DistrictDescription::new(
= Coordinates::new(Coordinate::new(-3), Coordinate::new(-2)),
- 13,
- 7,
- String::default(),
+ DistrictVariant {
+ length: 13,
+ width: 7,
+ number: 0,
+ },
= );
=
= assert_eq!(district.east(), Coordinate::new(10));
@@ -622,19 +706,27 @@ mod district_tests {
=
= #[test]
= fn into_rect() {
- let district = District::new(Coordinates::default(), 10, 20, String::default());
+ let variant = DistrictVariant {
+ length: 10,
+ width: 20,
+ number: 0,
+ };
+ let district = DistrictDescription::new(Coordinates::default(), variant);
= let rect = Rect::from(&district);
= assert_eq!(rect.min, Vec2::ZERO);
= assert_eq!(rect.max, Vec2::new(100.0, 200.0));
=
- let district = District::new(
+ let variant = DistrictVariant {
+ length: 10,
+ width: 20,
+ number: 0,
+ };
+ let district = DistrictDescription::new(
= Coordinates::new(
= Coordinate::<Longitude>::new(10),
= Coordinate::<Latitude>::new(5),
= ),
- 10,
- 20,
- String::default(),
+ variant,
= );
= let rect = Rect::from(&district);
= assert_eq!(rect.min, Vec2::new(100.0, 50.0));
@@ -643,17 +735,25 @@ mod district_tests {
=
= #[test]
= fn center() {
- let district = District::new(Coordinates::default(), 10, 20, String::default());
+ let variant = DistrictVariant {
+ length: 10,
+ width: 20,
+ number: 0,
+ };
+ let district = DistrictDescription::new(Coordinates::default(), variant);
= assert_eq!(district.center(), Vec3::new(50.0, 0.0, 100.0));
=
- let district = District::new(
+ let variant = DistrictVariant {
+ length: 10,
+ width: 15,
+ number: 0,
+ };
+ let district = DistrictDescription::new(
= Coordinates::new(
= Coordinate::<Longitude>::new(10),
= Coordinate::<Latitude>::new(5),
= ),
- 10,
- 15,
- String::default(),
+ variant,
= );
= assert_eq!(district.center(), Vec3::new(150.0, 0.0, 125.0));
= }
@@ -675,29 +775,32 @@ mod district_tests {
= let b = Rect::new(2., 0., 4., 2.);
= assert!(a.intersect(b).is_empty());
=
- // Contained
- let a = District {
+ let a = DistrictDescription {
= origin: Coordinates::new(
= Coordinate::<Longitude>::new(-60),
= Coordinate::<Latitude>::new(-49),
= ),
- length: 30,
- width: 49,
+ variant: DistrictVariant {
+ length: 30,
+ width: 49,
+ number: 0,
+ },
= rotation: Rotation::CW270,
- model: "district-490x300-a".to_string(),
= };
- let b = District {
+ let b = DistrictDescription {
= origin: Coordinates::new(
= Coordinate::<Longitude>::new(-60),
= Coordinate::<Latitude>::new(-18),
= ),
- length: 30,
- width: 18,
+ variant: DistrictVariant {
+ length: 18,
+ width: 30,
+ number: 0,
+ },
= rotation: Rotation::CW90,
- model: "district-180x300-b".to_string(),
= };
=
- assert!(a.overlaps(&b));
- assert!(b.overlaps(&a));
+ assert!(a.overlaps(&b).not());
+ assert!(b.overlaps(&a).not());
= }
=}