Week 18 of 2024

Development log of Otterhide

4 items
  1. Let users cycle inspector panels with I key
  2. Allow selecting via inspector panels
  3. Implement Adult and Senior components for persons
  4. Implement sex and reproduction systems

Let users cycle inspector panels with I key

On by Tad Lispy

The InspectorPanel is now a resource. If it exists, appropriate panel is displayed. Pressing the I key changes the panel or disables it.

index 1fc935f..0a224ea 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -21,6 +21,7 @@ use crate::population::Savings;
=use crate::population::Sex;
=use crate::ron_export;
=use crate::simulation::SimulationParameters;
+use bevy::input::common_conditions::input_just_released;
=use bevy::prelude::*;
=use bevy_egui::egui;
=use bevy_egui::egui::epaint::Shadow;
@@ -65,6 +66,11 @@ impl Plugin for HudPlugin {
=            .register_type::<DateSliderValue>()
=            .init_resource::<DateSliderValue>()
=            .register_type::<InspectorPanel>()
+            .add_systems(Startup, initialize_inspector_panel)
+            .add_systems(
+                Update,
+                cycle_inspector_panels.run_if(input_just_released(KeyCode::KeyI)),
+            )
=            .add_systems(
=                PostUpdate,
=                (
@@ -78,20 +84,33 @@ impl Plugin for HudPlugin {
=    }
=}
=
+fn initialize_inspector_panel(configuration: Res<Configuration>, mut commands: Commands) {
+    if let Some(panel) = configuration.inspect {
+        commands.insert_resource(panel)
+    };
+}
+
+fn cycle_inspector_panels(panel: Option<Res<InspectorPanel>>, mut commands: Commands) {
+    match panel.as_deref() {
+        None => commands.insert_resource(InspectorPanel::Buildings),
+        Some(InspectorPanel::Buildings) => commands.insert_resource(InspectorPanel::Population),
+        Some(InspectorPanel::Population) => commands.remove_resource::<InspectorPanel>(),
+    };
+}
+
=#[derive(
-    Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Display, ValueEnum, Reflect,
+    Resource, Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Display, ValueEnum, Reflect,
=)]
=pub enum InspectorPanel {
=    Population,
=    Buildings,
=}
=
-fn inspector_panel_shown(panel: &InspectorPanel) -> impl FnMut(Res<Configuration>) -> bool + '_ {
-    move |configuration: Res<Configuration>| {
-        configuration
-            .inspect
-            .map(|shown| panel == &shown)
-            .unwrap_or_default()
+fn inspector_panel_shown(
+    required_panel: &InspectorPanel,
+) -> impl FnMut(Option<Res<InspectorPanel>>) -> bool + '_ {
+    move |selected_panel: Option<Res<InspectorPanel>>| {
+        selected_panel.as_deref() == Some(required_panel)
=    }
=}
=

Allow selecting via inspector panels

On by Tad Lispy

The population and buildings inspector panels now contain links. Clicking those links select associated entities. Getting data to display those links can fail. To reuse the error display logic, the ui_error macro has be separated from selecting module into the new egui_widgets module.

new file mode 100644
index 0000000..23c939e
--- /dev/null
+++ b/src/egui_widgets.rs
@@ -0,0 +1,25 @@
+/// A collection of custom widgets
+use bevy_egui::egui;
+use bevy_egui::egui::{Color32, RichText};
+
+pub fn ui_error(ui: &mut egui::Ui, text: String) -> egui::Response {
+    ui.label(RichText::new(text).monospace().color(Color32::RED))
+}
+
+pub trait WithWidgets {
+    fn error(&mut self, text: String) -> egui::Response;
+}
+
+impl WithWidgets for egui::Ui {
+    fn error(&mut self, text: String) -> egui::Response {
+        self.label(RichText::new(text).monospace().color(Color32::RED))
+    }
+}
+
+#[macro_export]
+macro_rules! ui_error {
+    ($ui: expr, $($arg:tt)*) => {
+        let formatted = format!($($arg)*);
+        $ui.error(formatted)
+    };
+}
index 0a224ea..e12fae5 100644
--- a/src/heads_up_display.rs
+++ b/src/heads_up_display.rs
@@ -7,6 +7,7 @@ use crate::buildings::Residents;
=use crate::configuration::Configuration;
=use crate::date::{Date, DateSystems};
=use crate::day_month::Duration;
+use crate::egui_widgets::WithWidgets;
=use crate::history::Future;
=use crate::history::History;
=use crate::history::HistorySystems;
@@ -20,21 +21,24 @@ use crate::population::Residence;
=use crate::population::Savings;
=use crate::population::Sex;
=use crate::ron_export;
+use crate::selecting::SelectingSystems;
=use crate::simulation::SimulationParameters;
=use bevy::input::common_conditions::input_just_released;
=use bevy::prelude::*;
=use bevy_egui::egui;
=use bevy_egui::egui::epaint::Shadow;
+use bevy_egui::egui::Align;
=use bevy_egui::egui::Color32;
=use bevy_egui::egui::Frame;
+use bevy_egui::egui::Layout;
=use bevy_egui::egui::Margin;
=use bevy_egui::egui::ProgressBar;
+use bevy_egui::egui::RichText;
=use bevy_egui::egui::Slider;
=use bevy_egui::egui::Stroke;
=use bevy_egui::EguiContexts;
=use clap::ValueEnum;
=use derive_more::Display;
-use itertools::Itertools;
=use serde::{Deserialize, Serialize};
=use std::borrow::Borrow;
=
@@ -376,7 +380,8 @@ fn paint_population_panel(
=    buildings: Query<&Name, With<Building>>,
=    persons_register: Res<PersonsRegister>,
=    buildings_register: Res<BuildingsRegister>,
-
+    selecting_systems: Res<SelectingSystems>,
+    mut commands: Commands,
=    date: Res<Date>,
=) {
=    egui::CentralPanel::default()
@@ -411,19 +416,45 @@ fn paint_population_panel(
=                    for (id, name, sex, BirthDate(birth_date), Savings(savings), residence) in
=                        persons.iter()
=                    {
-                        let address = residence
-                            .and_then(|Residence(id)| buildings_register.0.get(id))
-                            .map(|entity| buildings.get(*entity).unwrap().as_str())
-                            .unwrap_or("-");
+                        // Name
+                        if ui.link(name.to_string()).clicked() {
+                            commands
+                                .run_system_with_input(selecting_systems.select, id.clone().into())
+                        }
+
=
=                        let age = Duration::new(birth_date, date.0.borrow()).years();
=
-                        ui.label(name.to_string());
=                        ui.label(sex.to_string());
=                        ui.label(age.to_string());
=                        ui.label(format!("{savings:.2} Œ"));
-                        ui.label(address);
-                        ui.label(id.0.to_string());
+
+                        // Address
+                        if let Some(Residence(residence_id)) = residence {
+                            if let Some(building_entity) = buildings_register.0.get(residence_id) {
+                                match buildings.get(*building_entity) {
+                                    Ok(address) => {
+                                        if ui.link(address.as_str()).clicked() {
+                                            commands.run_system_with_input(
+                                                selecting_systems.select,
+                                                residence_id.clone().into(),
+                                            );
+                                        }
+                                    },
+                                    Err(error) => {
+                                        ui_error!(ui, "Can't find {residence_id:?} among buildings: {error:#?}");
+                                    },
+                                }
+                            } else {
+                                ui_error!(ui, "Can't find {residence_id:?} in the buildings register.");
+                            }
+                        } else {
+                            ui.label(RichText::new("No known address").italics());
+                        }
+                        if ui.link(id.0.to_string()).clicked() {
+                            commands
+                                .run_system_with_input(selecting_systems.select, id.clone().into())
+                        }
=                        ui.end_row();
=                    }
=                })
@@ -446,7 +477,8 @@ fn paint_buildings_panel(
=    persons: Query<(&Name, &BirthDate, &Sex), With<Person>>,
=    buildings_register: Res<BuildingsRegister>,
=    persons_register: Res<PersonsRegister>,
-
+    selecting_systems: Res<SelectingSystems>,
+    mut commands: Commands,
=    date: Res<Date>,
=) {
=    // TODO: DRY on inspector panels layout ...
@@ -471,45 +503,72 @@ fn paint_buildings_panel(
=            ui.heading(format!("The {count} ({registered}) Buildings of Otterhide"));
=            egui::ScrollArea::both().show(ui, |ui| {
=                // TODO: Use Table from egui_extras
-                egui::Grid::new("persons-grid").show(ui, |ui| {
-                    ui.label("Address");
-                    ui.label("Class");
-                    ui.label("Capacity");
-                    ui.label("Residents:");
-                    ui.label("Id");
-                    ui.end_row();
-
-                    for (id, address, BuildingClass(class), HousingCapacity(capacity), residents) in
-                        buildings.iter()
-                    {
-                        // let address = residence
-                        //     .and_then(|Residence(id)| buildings_register.0.get(id))
-                        //     .map(|entity| buildings.get(*entity).unwrap().as_str())
-                        //     .unwrap_or("-");
-
-                        // let age = Duration::new(birth_date, date.0.borrow()).years();
-                        let residents = residents
-                            .map(|residents| residents.0.clone())
-                            .unwrap_or_default()
-                            .iter()
-                            .map(|id| persons_register.0.get(id))
-                            .flatten()
-                            .map(|entity| persons.get(*entity).ok())
-                            .flatten()
-                            .map(|(name, birthdate, sex)| {
-                                let age = Duration::new(&birthdate.0, &date.0).years();
-                                format!("{sex} {age:02} {name}")
-                            })
-                            .join("\n");
-
-                        ui.label(address.to_string());
-                        ui.label(class);
-                        ui.label(capacity.to_string());
-                        ui.label(residents);
-                        ui.label(id.0.to_string());
+                egui::Grid::new("persons-grid")
+                    .striped(true)
+                    .show(ui, |ui| {
+                        ui.label("Address");
+                        ui.label("Class");
+                        ui.label("Capacity");
+                        ui.label("Residents:");
+                        ui.label("Id");
=                        ui.end_row();
-                    }
-                })
+
+                        for (
+                            id,
+                            address,
+                            BuildingClass(class),
+                            HousingCapacity(capacity),
+                            residents,
+                        ) in buildings.iter()
+                        {
+
+
+                            if ui.link(address.to_string()).clicked() {
+                                commands.run_system_with_input(
+                                    selecting_systems.select,
+                                    id.clone().into(),
+                                )
+                            }
+
+                            ui.label(class);
+
+                            // Residents
+                            if let Some(Residents(residents)) = residents {
+                                ui.with_layout(Layout::top_down(Align::TOP), |ui| {
+
+                                    for resident_id in residents {
+                                        if let Some(resident_entity) = persons_register.0.get(resident_id) {
+                                            if let Ok((name, BirthDate(birth_date), sex)) = persons.get(*resident_entity) {
+                                                let age = Duration::new(&birth_date, &date.0).years();
+                                                let text = format!("{sex} {age:02} {name}");
+                                                if ui.link(text).clicked() {
+                                                    commands.run_system_with_input(selecting_systems.select, resident_id.clone().into());
+                                                }
+                                            } else {
+                                                ui_error!(ui, "Can't find {resident_id:?} among persons");
+                                            }
+                                        } else {
+                                            ui_error!(ui, "Can't find {resident_id:?} in the persons register.");
+                                        }
+                                    }
+
+                                });
+                            } else {
+                                ui.label(RichText::new("No residents").italics());
+                            };
+
+                            ui.label(capacity.to_string());
+
+                            // Id
+                            if ui.link(id.0.to_string()).clicked() {
+                                commands.run_system_with_input(
+                                    selecting_systems.select,
+                                    id.clone().into(),
+                                )
+                            }
+                            ui.end_row();
+                        }
+                        })
=            })
=        });
=}
index 49aaab0..c257ac6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,6 +5,8 @@ pub mod coordinates;
=pub mod date;
=pub mod day_month;
=pub mod districts;
+#[macro_use]
+pub mod egui_widgets;
=pub mod ground;
=pub mod heads_up_display;
=pub mod history;
index 808b5ab..2eb8806 100644
--- a/src/selecting.rs
+++ b/src/selecting.rs
@@ -17,17 +17,6 @@ impl Plugin for SelectingPlugin {
=    }
=}
=
-fn ui_error(ui: &mut egui::Ui, text: String) {
-    ui.label(RichText::new(text).monospace().color(Color32::RED));
-}
-
-macro_rules! ui_error {
-    ($ui: expr, $($arg:tt)*) => {
-        let formatted = format!($($arg)*);
-        ui_error($ui, formatted)
-    };
-}
-
=#[derive(Clone, Copy, Debug, From, Hash, PartialEq, Eq)]
=pub enum SelectedId {
=    Building(BuildingId),

Implement Adult and Senior components for persons

On by Tad Lispy

In preparation for reproduction systems.

index ddbcbe1..433f401 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -1,6 +1,6 @@
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
-use std::ops::{Div, Neg};
+use std::ops::{Div, Neg, Sub};
=
=const HOUR: f32 = 1.0 / 24.0;
=const MINUTE: f32 = HOUR / 60.0;
@@ -179,6 +179,15 @@ impl From<&DayMonth> for f32 {
=    }
=}
=
+impl Sub<&DayMonth> for &DayMonth {
+    type Output = Duration;
+
+    fn sub(self, rhs: &DayMonth) -> Self::Output {
+        let months: f32 = f32::from(self) - f32::from(rhs);
+        Duration::from_months(months)
+    }
+}
+
=#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
=pub struct Duration {
=    pub months: f32,
index 2ab87d3..eb3a5eb 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,6 +1,6 @@
=use crate::buildings::{BuildingId, BuildingsSystems, HasSpareCapacity, IsOvercrowded, Residents};
-use crate::date::SimulationFrameCounter;
=use crate::date::{self, Date};
+use crate::date::{NewMonth, SimulationFrameCounter};
=use crate::day_month::{DayMonth, Duration};
=use crate::history::Snapshot;
=use crate::major_state::MajorState;
@@ -21,6 +21,8 @@ pub struct PopulationPlugin;
=
=impl Plugin for PopulationPlugin {
=    fn build(&self, app: &mut App) {
+        let aging_systems = (grow_up, grow_old);
+
=        app.init_resource::<PersonsRegister>()
=            .register_type::<PersonsRegister>()
=            .register_type::<Person>()
@@ -47,6 +49,7 @@ impl Plugin for PopulationPlugin {
=                    .run_if(on_event::<Immigrated>())
=                    .before(plan_moving_into_houses),
=            )
+            .add_systems(Update, aging_systems.run_if(on_event::<NewMonth>()))
=            .add_systems(PreUpdate, remove_dead)
=            .add_systems(
=                Update,
@@ -87,9 +90,58 @@ pub struct Died {
=    pub person: PersonId,
=}
=
+
+#[derive(Component, Debug)]
+pub struct Adult;
+
+#[derive(Component, Debug)]
+pub struct Senior;
+
+impl Adult {
+    const AGE: f32 = 3.0;
+
+    fn is_adult(date: &Date, birth_date: &BirthDate) -> bool {
+        (&date.0 - &birth_date.0) > Duration::from_years(Self::AGE)
+    }
+}
+
+impl Senior {
+    const AGE: f32 = 8.0;
+
+    fn is_senior(date: &Date, birth_date: &BirthDate) -> bool {
+        (&date.0 - &birth_date.0) > Duration::from_years(Self::AGE)
+    }
+}
+
=#[derive(Component, Debug)]
=pub struct Dead;
=
+/// Mark children coming of age as Adult
+fn grow_up(
+    children: Query<(Entity, &BirthDate), Without<Adult>>,
+    date: Res<Date>,
+    mut commands: Commands,
+) {
+    for (child, birth_date) in children.iter() {
+        if Adult::is_adult(&date, birth_date) {
+            commands.entity(child).insert(Adult);
+        }
+    }
+}
+
+/// Mark aging adults as Senior
+fn grow_old(
+    persons: Query<(Entity, &BirthDate), (With<Adult>, Without<Senior>)>,
+    date: Res<Date>,
+    mut commands: Commands,
+) {
+    for (person, birth_date) in persons.iter() {
+        if Senior::is_senior(&date, &birth_date) {
+            commands.entity(person).insert(Senior);
+        };
+    }
+}
+
=/// Select people to die
=pub fn plan_mortality(
=    persons: Query<(&PersonId, &BirthDate, Option<&Residence>)>,
@@ -227,13 +279,21 @@ pub fn setup_new_person(
=    In(person): In<PersonDescription>,
=    mut commands: Commands,
=    mut register: ResMut<PersonsRegister>,
+    date: Res<Date>,
=) {
=    let id = person.id.clone();
=    let residence = person.residence.clone();
+    let birth_date = person.birth_date.clone();
=    let mut entity = commands.spawn(PersonBundle::from(person));
=    if let Some(building_id) = residence {
=        entity.insert(Residence(building_id.clone()));
=    };
+    if Adult::is_adult(&date, &birth_date) {
+        entity.insert(Adult);
+    };
+    if Senior::is_senior(&date, &birth_date) {
+        entity.insert(Senior);
+    };
=    register.0.insert(id, entity.id());
=}
=
index 2eb8806..01cadaa 100644
--- a/src/selecting.rs
+++ b/src/selecting.rs
@@ -1,11 +1,14 @@
=use crate::buildings::{BuildingId, BuildingsRegister, Residents};
-use crate::population::{PersonId, PersonsRegister, Residence, Sex};
+use crate::date::Date;
+use crate::egui_widgets::WithWidgets;
+use crate::population::{Adult, BirthDate, PersonId, PersonsRegister, Residence, Senior, Sex};
=use bevy::ecs::system::SystemId;
=use bevy::prelude::*;
=use bevy::utils::HashSet;
-use bevy_egui::egui::{Color32, RichText};
+use bevy_egui::egui::RichText;
=use bevy_egui::{egui, EguiContexts};
=use derive_more::From;
+use std::ops::Sub;
=
=pub struct SelectingPlugin;
=
@@ -13,7 +16,10 @@ impl Plugin for SelectingPlugin {
=    fn build(&self, app: &mut App) {
=        app.init_resource::<Selected>()
=            .add_systems(Startup, register_selecting_systems)
-            .add_systems(Update, draw_selected_windows);
+            .add_systems(
+                Update,
+                draw_selected_windows.run_if(resource_exists::<Date>),
+            );
=    }
=}
=
@@ -52,7 +58,18 @@ fn draw_selected_windows(
=    buildings_register: Res<BuildingsRegister>,
=    buildings: Query<(&Name, Option<&Residents>), With<BuildingId>>,
=    persons_register: Res<PersonsRegister>,
-    persons: Query<(&Name, &Sex, Option<&Residence>), With<PersonId>>,
+    persons: Query<
+        (
+            &Name,
+            &Sex,
+            Option<&Residence>,
+            &BirthDate,
+            Option<&Adult>,
+            Option<&Senior>,
+        ),
+        With<PersonId>,
+    >,
+    date: Res<Date>,
=) {
=    selected.ids.retain(|id| {
=        let mut window_is_open = true;
@@ -78,13 +95,12 @@ fn draw_selected_windows(
=                            for resident_id in residents.0.iter() {
=                                let Some(resident_entity) = persons_register.0.get(resident_id)
=                                else {
-                                    // TODO: Implement a error_label! macro similar to format!
=                                    ui_error!(ui, "Can't find resident {resident_id:?} in the persons register");
=                                    continue
=                                };
-                                let Ok((name, sex, _residence)) = persons.get(*resident_entity)
+                                let Ok((name, sex, _residence, _birth_date, _adult, _senior)) = persons.get(*resident_entity)
=                                else {
-                                    ui_error!(ui, "Can't find resident {resident_id:?} in the persons register");
+                                    ui_error!(ui, "Can't find resident {resident_id:?} among persons");
=                                    continue
=                                };
=                                if ui.link(format!("{sex} {name}")).clicked() {
@@ -96,20 +112,32 @@ fn draw_selected_windows(
=                            }
=                        } else {
=                            ui.label(RichText::new("No residents").italics());
-                        }
+                        };
+                        ui.label(building_id.to_string());
=                    });
=            }
=            SelectedId::Person(person_id) => {
=                let Some(person_entity) = persons_register.0.get(person_id) else {
=                    return true;
=                };
-                let Ok((name, _sex, residence)) = persons.get(*person_entity) else {
+                let Ok((name, _sex, residence, birth_date, adult, senior)) = persons.get(*person_entity) else {
=                    return true;
=                };
=
=                egui::Window::new(name.to_string())
=                    .open(&mut window_is_open)
=                    .show(contexts.ctx_mut(), |ui| {
+                        let age: u16 = date.0.sub(&birth_date.0).months as u16 / 12;
+                        let sex_label = match _sex {
+                            Sex::Male => "male",
+                            Sex::Female => "female",
+                        };
+                        match (adult, senior) {
+                            (None, None) => ui.label(format!("{age} year old child")),
+                            (None, Some(_)) => ui.error("Old child".to_string()),
+                            (Some(_), None) => ui.label(format!("{age} year old {sex_label}")),
+                            (Some(_), Some(_)) => ui.label(format!("{age} year old senior {sex_label}")),
+                        };
=                        ui.heading("Residence");
=                        if let Some(residence) = residence {
=                            let Some(residence_entity) = buildings_register.0.get(&residence.0)
@@ -129,7 +157,9 @@ fn draw_selected_windows(
=                            };
=                        } else {
=                            ui.label(RichText::new("No known residence").italics());
-                        }
+                        };
+
+                        ui.label(person_id.to_string());
=                    });
=            }
=        }

Implement sex and reproduction systems

On by Tad Lispy

People of opposite sex residing in the same building can have children.

index b1da5b5..fa3fda0 100644
--- a/src/bin/otterhide-to-sqlite.rs
+++ b/src/bin/otterhide-to-sqlite.rs
@@ -3,7 +3,7 @@ use clap::Parser;
=use otterhide::buildings::ConstructionOrder;
=use otterhide::districts::NewDistrictEstablished;
=use otterhide::history::{EventLogEntry, HistoricalEvent, History};
-use otterhide::population::{Died, Immigrated, MovedIn, MovedOut};
+use otterhide::population::{ChildIsBorn, Died, Immigrated, MovedIn, MovedOut};
=use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous};
=use std::path::PathBuf;
=
@@ -117,9 +117,25 @@ async fn main() -> anyhow::Result<()> {
=                .bind(person.id.to_string())
=                .bind(date.year())
=                .bind(date.month() as u8)
+            }
+            HistoricalEvent::ChildIsBorn(ChildIsBorn { child }) => {
+                sqlx::query(
+                    "Insert into person (
+                               id,
+                               name,
+                               sex,
+                               birth_year,
+                               birth_month
+                             ) values (?, ?, ?, ?, ?)",
+                )
+                .bind(child.id.to_string())
+                .bind(child.name.to_string())
+                .bind(child.sex.to_string())
+                .bind(child.birth_date.0.year())
+                .bind(child.birth_date.0.month() as u8)
=                .execute(&pool)
=                .await
-                .context("Inserting immigration data")?;
+                .context("Inserting a new born person data")?;
=            }
=            HistoricalEvent::Died(Died { person }) => {
=                sqlx::query(
index 8253f29..3bca7cf 100644
--- a/src/buildings.rs
+++ b/src/buildings.rs
@@ -4,6 +4,7 @@ use crate::history::Snapshot;
=use crate::major_state::MajorState;
=use crate::major_state::PreloadedAssets;
=use crate::parcels::{Parcel, ParcelNumber};
+use crate::population::ChildIsBorn;
=use crate::population::MovedOut;
=use crate::population::{MovedIn, Person, PersonId};
=use crate::selecting::Selected;
@@ -47,6 +48,10 @@ impl Plugin for BuildingsPlugin {
=                        .and_then(resource_changed::<SimulationFrameCounter>),
=                ),
=            )
+            .add_systems(
+                Update,
+                register_newborn_residents.run_if(on_event::<ChildIsBorn>()),
+            )
=            .add_systems(
=                Update,
=                move_into_houses
@@ -63,6 +68,21 @@ impl Plugin for BuildingsPlugin {
=    }
=}
=
+fn register_newborn_residents(
+    mut child_birth: EventReader<ChildIsBorn>,
+    systems: Res<BuildingsSystems>,
+    mut commands: Commands,
+) {
+    for ChildIsBorn { child } in child_birth.read() {
+        if let Some(residence) = child.residence {
+            let moved_in = MovedIn {
+                who: child.id,
+                where_to: residence,
+            };
+            commands.run_system_with_input(systems.register_resident, moved_in);
+        }
+    }
+}
=fn move_into_houses(
=    mut moved_in: EventReader<MovedIn>,
=    systems: Res<BuildingsSystems>,
index 433f401..9c86d8f 100644
--- a/src/day_month.rs
+++ b/src/day_month.rs
@@ -208,7 +208,7 @@ impl Duration {
=        }
=    }
=
-    pub fn from_months(months: f32) -> Self {
+    pub const fn from_months(months: f32) -> Self {
=        Self { months }
=    }
=
@@ -224,7 +224,7 @@ impl Duration {
=        }
=    }
=
-    pub fn years(&self) -> i32 {
+    pub const fn years(&self) -> i32 {
=        self.months as i32 / 12
=    }
=}
index fddff06..996d19c 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -5,7 +5,7 @@ use crate::districts::DistrictsSnapshot;
=use crate::districts::NewDistrictEstablished;
=use crate::ground::GroundSnapshot;
=use crate::major_state::{MajorState, ReadyPredicates};
-use crate::population::{Died, Immigrated, MovedIn, MovedOut, PopulationSnapshot};
+use crate::population::{ChildIsBorn, Died, Immigrated, MovedIn, MovedOut, PopulationSnapshot};
=use crate::roads::RoadsSnapshot;
=use bevy::ecs::system::{RunSystemOnce, SystemId};
=use bevy::prelude::*;
@@ -89,6 +89,7 @@ pub enum HistoricalEvent {
=    ConstructionOrder(ConstructionOrder),
=    NewDistrictEstablished(NewDistrictEstablished),
=    Immigrated(Immigrated),
+    ChildIsBorn(ChildIsBorn),
=    Died(Died),
=    MovedIn(MovedIn),
=    MovedOut(MovedOut),
@@ -130,6 +131,9 @@ impl Display for HistoricalEvent {
=            HistoricalEvent::MovedOut(MovedOut { who, where_from }) => {
=                write!(f, "The person {who} moved out of the building {where_from}")
=            }
+            HistoricalEvent::ChildIsBorn(ChildIsBorn { child }) => {
+                write!(f, "The child is born {child:#?}")
+            }
=            HistoricalEvent::Died(Died { person: id }) => {
=                write!(f, "The person {id} died")
=            }
@@ -287,6 +291,7 @@ fn replay_historical_events(
=    mut immigrated: EventWriter<Immigrated>,
=    mut moved_in: EventWriter<MovedIn>,
=    mut moved_out: EventWriter<MovedOut>,
+    mut child_birth: EventWriter<ChildIsBorn>,
=    mut died: EventWriter<Died>,
=) {
=    future.events.retain(|entry| {
@@ -309,6 +314,9 @@ fn replay_historical_events(
=                HistoricalEvent::MovedOut(event) => {
=                    moved_out.send(event);
=                }
+                HistoricalEvent::ChildIsBorn(event) => {
+                    child_birth.send(event);
+                }
=                HistoricalEvent::Died(event) => {
=                    died.send(event);
=                }
index eb3a5eb..2665046 100644
--- a/src/population.rs
+++ b/src/population.rs
@@ -1,4 +1,6 @@
-use crate::buildings::{BuildingId, BuildingsSystems, HasSpareCapacity, IsOvercrowded, Residents};
+use crate::buildings::{
+    Building, BuildingId, BuildingsSystems, HasSpareCapacity, IsOvercrowded, Residents,
+};
=use crate::date::{self, Date};
=use crate::date::{NewMonth, SimulationFrameCounter};
=use crate::day_month::{DayMonth, Duration};
@@ -15,13 +17,18 @@ use rand::distributions::Standard;
=use rand::prelude::*;
=use serde::{Deserialize, Serialize};
=use std::fmt::Display;
-use std::ops::Neg;
+use std::ops::{Neg, Not};
=
=pub struct PopulationPlugin;
=
=impl Plugin for PopulationPlugin {
=    fn build(&self, app: &mut App) {
=        let aging_systems = (grow_up, grow_old);
+        let reproduction_systems = (
+            get_pregnant.run_if(in_state(MajorState::Simulating)),
+            give_birth.run_if(in_state(MajorState::Simulating)),
+            spawn_children.run_if(on_event::<ChildIsBorn>()),
+        );
=
=        app.init_resource::<PersonsRegister>()
=            .register_type::<PersonsRegister>()
@@ -33,6 +40,7 @@ impl Plugin for PopulationPlugin {
=            .add_event::<Immigrated>()
=            .add_event::<MovedIn>()
=            .add_event::<MovedOut>()
+            .add_event::<ChildIsBorn>()
=            .add_event::<Died>()
=            .add_systems(Startup, register_population_systems)
=            .add_systems(
@@ -50,6 +58,7 @@ impl Plugin for PopulationPlugin {
=                    .before(plan_moving_into_houses),
=            )
=            .add_systems(Update, aging_systems.run_if(on_event::<NewMonth>()))
+            .add_systems(Update, reproduction_systems)
=            .add_systems(PreUpdate, remove_dead)
=            .add_systems(
=                Update,
@@ -90,6 +99,129 @@ pub struct Died {
=    pub person: PersonId,
=}
=
+#[derive(Component, Debug)]
+pub struct Pregnant {
+    father: PersonId,
+    since: DayMonth,
+}
+
+#[derive(Event, Debug, Serialize, Deserialize, Clone)]
+pub struct ChildIsBorn {
+    pub child: PersonDescription,
+}
+
+const PREGNANCY_PROBABILITY: f64 = 0.01;
+const PREGNANCY_DURATION: Duration = Duration::from_months(9.0);
+
+fn get_pregnant(
+    persons: Query<&Sex, (With<Adult>, Without<Senior>, Without<Pregnant>)>,
+    buildings: Query<&Residents, With<Building>>,
+    persons_register: Res<PersonsRegister>,
+    date: Res<Date>,
+    mut commands: Commands,
+) {
+    for residents in buildings.iter() {
+        for partner_a_id in residents.0.iter() {
+            if thread_rng().gen_bool(PREGNANCY_PROBABILITY).not() {
+                continue;
+            }
+            let Some(partner_a_entity) = persons_register.0.get(partner_a_id) else {
+                error!("Can't find entity for {partner_a_id:?} in the persons register.");
+                continue;
+            };
+            let Ok(partner_a_sex) = persons.get(*partner_a_entity) else {
+                // Not in reproductive age or already pregnant. Ignore.
+                continue;
+            };
+            if *partner_a_sex != Sex::Female {
+                continue;
+            }
+            let Some(partner_b_id) = residents.0.iter().choose(&mut thread_rng()) else {
+                error!("Failed to select a random person from residents. Is it empty? Then the component should be removed.");
+                continue;
+            };
+            let Some(partner_b_entity) = persons_register.0.get(partner_b_id) else {
+                error!("Can't find entity for {partner_b_id:?} in the persons register.");
+                continue;
+            };
+            let Ok(partner_b_sex) = persons.get(*partner_b_entity) else {
+                // Not in reproductive age or already pregnant. Ignore.
+                continue;
+            };
+            if *partner_b_sex != Sex::Male {
+                continue;
+            }
+
+            debug!("{partner_a_id:?} got pregnant with {partner_b_id:?}");
+            commands.entity(*partner_a_entity).insert(Pregnant {
+                since: date.0,
+                father: *partner_b_id,
+            });
+        }
+    }
+}
+
+fn give_birth(
+    pregnant_mothers: Query<(
+        Entity,
+        &PersonId,
+        &Pregnant,
+        Option<&Residence>,
+        &PersonName,
+    )>,
+    fathers: Query<&PersonName, Without<Pregnant>>,
+    date: Res<Date>,
+    persons_register: Res<PersonsRegister>,
+    mut child_birth: EventWriter<ChildIsBorn>,
+    mut commands: Commands,
+) {
+    for (mother_entity, mother_id, pregnant, residence, mother_name) in pregnant_mothers.iter() {
+        if &date.0 - &pregnant.since > PREGNANCY_DURATION {
+            commands.entity(mother_entity).remove::<Pregnant>();
+
+            // TODO: How to get fathers name after they are dead?
+            let fathers_name: Option<&PersonName> = persons_register
+                .0
+                .get(&pregnant.father)
+                .and_then(|father_entity| fathers.get(*father_entity).ok());
+
+            let rng = &mut thread_rng();
+            let id = Uuid::from_bytes(rng.gen()).into();
+            let sex = rng.gen::<Sex>();
+            let first_names = match sex {
+                Sex::Male => MALE_NAMES,
+                Sex::Female => FEMALE_NAMES,
+            };
+            let name = {
+                PersonName {
+                    first: first_names.choose(rng).unwrap().to_string(),
+                    second: mother_name.second.clone(),
+                    third: fathers_name.map(|name| name.second.clone()),
+                }
+            };
+            let child = PersonDescription {
+                id,
+                name,
+                sex,
+                savings: Savings(0.0),
+                residence: residence.map(|residence| residence.0),
+                birth_date: date.0.into(),
+            };
+            child_birth.send(ChildIsBorn { child });
+        }
+    }
+}
+
+fn spawn_children(
+    mut child_births: EventReader<ChildIsBorn>,
+    systems: Res<PopulationSystems>,
+    mut commands: Commands,
+) {
+    for ChildIsBorn { child } in child_births.read() {
+        debug!("A child is born: {child:#?}");
+        commands.run_system_with_input(systems.setup_new_person, child.clone());
+    }
+}
=
=#[derive(Component, Debug)]
=pub struct Adult;
@@ -212,6 +344,9 @@ pub struct PersonDescription {
=    pub savings: Savings,
=    pub residence: Option<BuildingId>,
=    pub birth_date: BirthDate,
+    // TODO: Store information about each person's parents.
+    // pub mother: Option<PersonId>,
+    // pub father: Option<PersonId>,
=}
=
=/// A statistical distribution that holds the current date
@@ -475,7 +610,7 @@ pub struct Savings(pub f32);
=#[derive(Component, Clone, Debug, From, Serialize, Deserialize)]
=pub struct BirthDate(pub DayMonth);
=
-#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
+#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
=pub enum Sex {
=    Male,
=    Female,