Repository: Leafwing-Studios/Emergence
Pull request #790 (2023-04-27 15:47:04)

Draw water from plants
Progress towards #772.

Still need to actually incorporate water into the crafting process, but that can come in the next PR once some critical bugs are fixed.


Patch:
diff --git a/emergence_game/assets/manifests/base_game.structure_manifest.json b/emergence_game/assets/manifests/base_game.structure_manifest.json
index a21fb78..2217756 100644
--- a/emergence_game/assets/manifests/base_game.structure_manifest.json
+++ b/emergence_game/assets/manifests/base_game.structure_manifest.json
@@ -126,6 +126,10 @@
 					"starting_recipe": "acacia_leaf_production"
 				}
 			},
+			"root_zone": {
+				"max_depth": 1.5,
+				"radius": 1
+			},
 			"construction_strategy": {
 				"Seedling": "acacia_seedling"
 			},
@@ -156,6 +160,10 @@
 			"construction_strategy": {
 				"Seedling": "acacia_seedling"
 			},
+			"root_zone": {
+				"max_depth": 3,
+				"radius": 2
+			},
 			"max_workers": 6,
 			"passable": false
 		},
diff --git a/emergence_game/assets/manifests/base_game.unit_manifest.json b/emergence_game/assets/manifests/base_game.unit_manifest.json
index fc31b64..578e319 100644
--- a/emergence_game/assets/manifests/base_game.unit_manifest.json
+++ b/emergence_game/assets/manifests/base_game.unit_manifest.json
@@ -20,8 +20,8 @@
         "item": "leuco_chunk",
         "energy": 50.0
       },
-      "max_impatience": 10,
-      "max_age": 10.0,
+      "max_impatience": 5,
+      "max_age": 30.0,
       "wandering_behavior": {
         "wander_durations": [
           [
diff --git a/emergence_lib/src/simulation/geometry.rs b/emergence_lib/src/simulation/geometry.rs
index b7bb94c..c82a0f4 100644
--- a/emergence_lib/src/simulation/geometry.rs
+++ b/emergence_lib/src/simulation/geometry.rs
@@ -268,8 +268,10 @@ impl TilePos {
 /// The discretized height of this tile
 ///
 /// The minimum height is 0.
-#[derive(Component, Clone, Copy, Debug, PartialEq, PartialOrd, Display, Default)]
-pub(crate) struct Height(pub f32);
+#[derive(
+    Component, Clone, Copy, Debug, PartialEq, PartialOrd, Display, Default, Serialize, Deserialize,
+)]
+pub struct Height(pub f32);
 
 impl Height {
     /// The absolute minimum height.
diff --git a/emergence_lib/src/simulation/time.rs b/emergence_lib/src/simulation/time.rs
index f6999a1..0b6b307 100644
--- a/emergence_lib/src/simulation/time.rs
+++ b/emergence_lib/src/simulation/time.rs
@@ -132,7 +132,7 @@ impl Default for InGameTime {
     fn default() -> Self {
         InGameTime {
             elapsed_time: Days(0.0),
-            seconds_per_day: 3.,
+            seconds_per_day: 300.,
         }
     }
 }
diff --git a/emergence_lib/src/structures/structure_manifest.rs b/emergence_lib/src/structures/structure_manifest.rs
index 3060278..23655f9 100644
--- a/emergence_lib/src/structures/structure_manifest.rs
+++ b/emergence_lib/src/structures/structure_manifest.rs
@@ -6,6 +6,7 @@ use crate::{
     crafting::components::{ActiveRecipe, RawActiveRecipe},
     items::item_manifest::Item,
     organisms::{OrganismId, OrganismVariety, RawOrganismVariety},
+    water::roots::RootZone,
 };
 use bevy::{
     reflect::{FromReflect, Reflect, TypeUuid},
@@ -58,6 +59,8 @@ pub struct StructureData {
     pub max_workers: u8,
     /// The tiles taken up by this building.
     pub footprint: Footprint,
+    /// The set of tiles that this structure can reach with its roots.
+    pub root_zone: Option<RootZone>,
     /// Can units pass over this structure?
     pub passable: bool,
 }
@@ -77,6 +80,8 @@ pub struct RawStructureData {
     pub max_workers: u8,
     /// The tiles taken up by this building.
     pub footprint: Option<Footprint>,
+    /// The set of tiles that this structure can reach with its roots.
+    pub root_zone: Option<RootZone>,
     /// Can units pass over this structure?
     pub passable: bool,
 }
@@ -89,6 +94,7 @@ impl From<RawStructureData> for StructureData {
             construction_strategy: raw.construction_strategy.into(),
             max_workers: raw.max_workers,
             footprint: raw.footprint.unwrap_or_default(),
+            root_zone: raw.root_zone,
             passable: raw.passable,
         }
     }
diff --git a/emergence_lib/src/ui/selection_details.rs b/emergence_lib/src/ui/selection_details.rs
index 68cb4ab..8715a2f 100644
--- a/emergence_lib/src/ui/selection_details.rs
+++ b/emergence_lib/src/ui/selection_details.rs
@@ -673,6 +673,10 @@ Tile: {tile_pos}"
                 string += &format!("\nStoring: {}", storage.display(item_manifest));
             }
 
+            if let Some(root_zone) = &structure_manifest.get(self.structure_id).root_zone {
+                string += &format!("\n{root_zone}",);
+            }
+
             if let Some(organism) = &self.maybe_organism_details {
                 string += &format!("\n{}", organism.display(structure_manifest, unit_manifest));
             };
diff --git a/emergence_lib/src/water/mod.rs b/emergence_lib/src/water/mod.rs
index 263c614..4940783 100644
--- a/emergence_lib/src/water/mod.rs
+++ b/emergence_lib/src/water/mod.rs
@@ -14,6 +14,10 @@ use crate::simulation::{
     SimulationSet,
 };
 
+use self::roots::draw_water_from_roots;
+
+pub mod roots;
+
 /// A plugin that handles water movement and behavior.
 pub(super) struct WaterPlugin;
 
@@ -24,6 +28,7 @@ impl Plugin for WaterPlugin {
                 evaporation,
                 precipitation,
                 horizontal_water_movement,
+                draw_water_from_roots,
                 update_surface_water_map_geometry,
             )
                 .in_set(SimulationSet)
@@ -142,7 +147,7 @@ fn evaporation(
     /// The amount of water that evaporates per day from each surface water tile.
     const EVAPORATION_PER_DAY: Height = Height(1.5);
     /// The relative rate of evaporation from soil relative to surface water.
-    const SOIL_EVAPORATION_RATE: f32 = 0.5;
+    const SOIL_EVAPORATION_RATE: f32 = 0.2;
 
     let evaporation_per_second = EVAPORATION_PER_DAY.0 / in_game_time.seconds_per_day();
     let elapsed_time = fixed_time.period.as_secs_f32();
@@ -170,7 +175,7 @@ fn precipitation(
     map_geometry: Res<MapGeometry>,
 ) {
     /// The amount of water that is deposited per day on each tile.
-    const PRECIPITATION_PER_DAY: Height = Height(1.0);
+    const PRECIPITATION_PER_DAY: Height = Height(0.5);
     let precipitation_per_second = PRECIPITATION_PER_DAY.0 / in_game_time.seconds_per_day();
     let elapsed_time = fixed_time.period.as_secs_f32();
 
diff --git a/emergence_lib/src/water/roots.rs b/emergence_lib/src/water/roots.rs
new file mode 100644
index 0000000..3128285
--- /dev/null
+++ b/emergence_lib/src/water/roots.rs
@@ -0,0 +1,99 @@
+//! Datastructures and mechanics for roots, which draw water from the nearby water table.
+
+use std::fmt::{Display, Formatter};
+
+use hexx::shapes::hexagon;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    asset_management::manifest::Id,
+    simulation::geometry::{Height, MapGeometry, TilePos},
+    structures::structure_manifest::{Structure, StructureManifest},
+};
+use bevy::prelude::*;
+
+use super::WaterTable;
+
+/// The volume around a tile that roots can draw water from.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct RootZone {
+    /// The depth from the surface beyond which roots cannot draw water.
+    pub max_depth: Height,
+    /// The radius of the root zone.
+    ///
+    /// Water can only be drawn from tiles within this radius.
+    pub radius: u32,
+}
+
+impl RootZone {
+    /// Returns the set of tiles that this root zone can reach, with water above the max depth.
+    fn relevant_tiles(
+        &self,
+        center: TilePos,
+        water_table: &WaterTable,
+        map_geometry: &MapGeometry,
+    ) -> Vec<TilePos> {
+        let hexagon = hexagon(center.hex, self.radius);
+        let mut relevant_tiles = Vec::with_capacity(hexagon.len());
+        for hex in hexagon {
+            let tile_pos = TilePos { hex };
+            if !map_geometry.is_valid(tile_pos) {
+                continue;
+            };
+
+            match water_table.depth_to_water_table(tile_pos, map_geometry) {
+                super::DepthToWaterTable::Flooded => relevant_tiles.push(tile_pos),
+                super::DepthToWaterTable::Dry => (),
+                super::DepthToWaterTable::Depth(depth) => {
+                    if depth <= self.max_depth {
+                        relevant_tiles.push(tile_pos);
+                    }
+                }
+            }
+        }
+
+        relevant_tiles
+    }
+}
+
+impl Display for RootZone {
+    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+        write!(
+            f,
+            "Root Zone: {} tiles deep, {} tiles radius",
+            self.max_depth, self.radius
+        )
+    }
+}
+
+/// Draws water from the water table if and only if the structure needs more water.
+// PERF: we could store RootZone as a component on the structure at the cost of some memory.
+// This would give us faster lookups, but force duplication.
+pub(super) fn draw_water_from_roots(
+    mut water_table: ResMut<WaterTable>,
+    structure_query: Query<(&TilePos, &Id<Structure>)>,
+    structure_manifest: Res<StructureManifest>,
+    map_geometry: Res<MapGeometry>,
+    fixed_time: Res<FixedTime>,
+) {
+    let water_draw_rate = 200.0; // TODO: make this a property of the structure.
+    let water_requested = water_draw_rate * fixed_time.period.as_secs_f32();
+
+    // TODO: only do this during CraftingState::NeedsInput
+    for (&center, &structure_id) in structure_query.iter() {
+        let structure_data = structure_manifest.get(structure_id);
+        if let Some(root_zone) = &structure_data.root_zone {
+            let relevant_tiles = root_zone.relevant_tiles(center, &water_table, &map_geometry);
+            let n = relevant_tiles.len() as f32;
+            let water_per_tile = Height(water_requested / n);
+
+            for tile_pos in relevant_tiles {
+                // This can ever so slightly overdraw water, but that's fine.
+                // Accounting for this would significantly complicate the code.
+                // Pretend it's capillary action or something!
+                water_table.subtract(tile_pos, water_per_tile);
+                // TODO: add water to the structure.
+            }
+        }
+    }
+}


Test patch:
diff --git a/emergence_lib/tests/serde_manifests.rs b/emergence_lib/tests/serde_manifests.rs
index 989b9b5..1378465 100644
--- a/emergence_lib/tests/serde_manifests.rs
+++ b/emergence_lib/tests/serde_manifests.rs
@@ -12,7 +12,7 @@ use emergence_lib::{
         lifecycle::{RawLifePath, RawLifecycle},
         RawOrganismId, RawOrganismVariety,
     },
-    simulation::light::Illuminance,
+    simulation::{geometry::Height, light::Illuminance},
     structures::{
         structure_manifest::{RawStructureData, RawStructureKind, RawStructureManifest},
         Footprint,
@@ -23,6 +23,7 @@ use emergence_lib::{
         unit_manifest::{RawUnitData, RawUnitManifest},
         WanderingBehavior,
     },
+    water::roots::RootZone,
 };
 use leafwing_abilities::prelude::Pool;
 
@@ -236,6 +237,7 @@ fn can_serialize_structure_manifest() {
                     },
                     max_workers: 6,
                     footprint: Some(Footprint::single()),
+                    root_zone: None,
                     passable: false,
                 },
             ),
@@ -250,6 +252,7 @@ fn can_serialize_structure_manifest() {
                     },
                     max_workers: 1,
                     footprint: Some(Footprint::path()),
+                    root_zone: None,
                     passable: true,
                 },
             ),
@@ -274,6 +277,7 @@ fn can_serialize_structure_manifest() {
                     },
                     max_workers: 1,
                     footprint: Some(Footprint::single()),
+                    root_zone: None,
                     passable: false,
                 },
             ),
@@ -297,6 +301,10 @@ fn can_serialize_structure_manifest() {
                     ),
                     max_workers: 1,
                     footprint: Some(Footprint::single()),
+                    root_zone: Some(RootZone {
+                        max_depth: Height(1.5),
+                        radius: 1,
+                    }),
                     passable: false,
                 },
             ),
@@ -316,6 +324,10 @@ fn can_serialize_structure_manifest() {
                     ),
                     max_workers: 6,
                     footprint: Some(Footprint::single()),
+                    root_zone: Some(RootZone {
+                        max_depth: Height(3.0),
+                        radius: 2,
+                    }),
                     passable: false,
                 },
             ),
@@ -332,6 +344,7 @@ fn can_serialize_structure_manifest() {
                     },
                     max_workers: 3,
                     footprint: Some(Footprint::hexagon(1)),
+                    root_zone: None,
                     passable: false,
                 },
             ),
@@ -349,6 +362,7 @@ fn can_serialize_structure_manifest() {
                     max_workers: 6,
                     // Forms a crescent shape
                     footprint: Some(Footprint::single()),
+                    root_zone: None,
                     passable: false,
                 },
             ),
@@ -366,6 +380,7 @@ fn can_serialize_structure_manifest() {
                     },
                     max_workers: 6,
                     footprint: Some(Footprint::single()),
+                    root_zone: None,
                     passable: false,
                 },
             ),