Repository: Leafwing-Studios/Emergence (rust)

Issue:
Make some fields in manifests optional
Many of these fields (e.g. Footprint, ProductionConditions, Seedling) have sensible defaults. We should allow users to not specify these, and infer their value instead.

Pull request:
Make more manifest fields optional
Fixes #652.

Patch:
diff --git a/emergence_game/assets/manifests/base_game.recipe_manifest.json b/emergence_game/assets/manifests/base_game.recipe_manifest.json
index 5830595..020b278 100644
--- a/emergence_game/assets/manifests/base_game.recipe_manifest.json
+++ b/emergence_game/assets/manifests/base_game.recipe_manifest.json
@@ -7,10 +7,8 @@
 			"outputs": {},
 			"craft_time": 10,
 			"conditions": {
-				"workers_required": 1,
-				"allowable_light_range": null
-			},
-			"energy": null
+				"workers_required": 1
+			}
 		},
 		"leuco_chunk_production": {
 			"inputs": {
@@ -20,10 +18,6 @@
 				"leuco_chunk": 1
 			},
 			"craft_time": 2,
-			"conditions": {
-				"workers_required": 0,
-				"allowable_light_range": null
-			},
 			"energy": 40.0
 		},
 		"acacia_leaf_production": {
@@ -66,10 +60,8 @@
 			},
 			"craft_time": 10,
 			"conditions": {
-				"workers_required": 2,
-				"allowable_light_range": null
-			},
-			"energy": null
+				"workers_required": 2
+			}
 		}
 	}
 }
\ No newline at end of file
diff --git a/emergence_game/assets/manifests/base_game.structure_manifest.json b/emergence_game/assets/manifests/base_game.structure_manifest.json
index 0473ae0..8451a35 100644
--- a/emergence_game/assets/manifests/base_game.structure_manifest.json
+++ b/emergence_game/assets/manifests/base_game.structure_manifest.json
@@ -1,7 +1,6 @@
 {
 	"structure_types": {
 		"hatchery": {
-			"organism_variety": null,
 			"kind": {
 				"Crafting": {
 					"starting_recipe": "hatch_ants"
@@ -18,24 +17,12 @@
 					]
 				}
 			},
-			"max_workers": 6,
-			"footprint": {
-				"set": [
-					{
-						"hex": {
-							"x": 0,
-							"y": 0
-						}
-					}
-				]
-			}
+			"max_workers": 6
 		},
 		"storage": {
-			"organism_variety": null,
 			"kind": {
 				"Storage": {
-					"max_slot_count": 3,
-					"reserved_for": null
+					"max_slot_count": 3
 				}
 			},
 			"construction_strategy": {
@@ -51,17 +38,7 @@
 					]
 				}
 			},
-			"max_workers": 6,
-			"footprint": {
-				"set": [
-					{
-						"hex": {
-							"x": 0,
-							"y": 0
-						}
-					}
-				]
-			}
+			"max_workers": 6
 		},
 		"acacia_seedling": {
 			"organism_variety": {
@@ -74,7 +51,6 @@
 							"new_form": {
 								"Structure": "acacia_sprout"
 							},
-							"energy_required": null,
 							"time_required": 1.0
 						}
 					]
@@ -104,17 +80,7 @@
 					]
 				}
 			},
-			"max_workers": 1,
-			"footprint": {
-				"set": [
-					{
-						"hex": {
-							"x": 0,
-							"y": 0
-						}
-					}
-				]
-			}
+			"max_workers": 1
 		},
 		"leuco": {
 			"organism_variety": {
@@ -139,7 +105,6 @@
 			},
 			"construction_strategy": {
 				"Direct": {
-					"work": null,
 					"materials": {
 						"leuco_chunk": 1
 					},
@@ -149,17 +114,7 @@
 					]
 				}
 			},
-			"max_workers": 6,
-			"footprint": {
-				"set": [
-					{
-						"hex": {
-							"x": 0,
-							"y": 0
-						}
-					}
-				]
-			}
+			"max_workers": 6
 		},
 		"acacia_sprout": {
 			"organism_variety": {
@@ -172,8 +127,7 @@
 							"new_form": {
 								"Structure": "acacia"
 							},
-							"energy_required": 500.0,
-							"time_required": null
+							"energy_required": 500.0
 						}
 					]
 				},
@@ -193,17 +147,7 @@
 			"construction_strategy": {
 				"Seedling": "acacia_seedling"
 			},
-			"max_workers": 1,
-			"footprint": {
-				"set": [
-					{
-						"hex": {
-							"x": 0,
-							"y": 0
-						}
-					}
-				]
-			}
+			"max_workers": 1
 		},
 		"acacia": {
 			"organism_variety": {
@@ -229,20 +173,9 @@
 			"construction_strategy": {
 				"Seedling": "acacia_seedling"
 			},
-			"max_workers": 6,
-			"footprint": {
-				"set": [
-					{
-						"hex": {
-							"x": 0,
-							"y": 0
-						}
-					}
-				]
-			}
+			"max_workers": 6
 		},
 		"ant_hive": {
-			"organism_variety": null,
 			"kind": {
 				"Crafting": {
 					"starting_recipe": "ant_egg_production"
diff --git a/emergence_lib/src/items/recipe.rs b/emergence_lib/src/items/recipe.rs
index 84dffc7..b1ffab1 100644
--- a/emergence_lib/src/items/recipe.rs
+++ b/emergence_lib/src/items/recipe.rs
@@ -113,7 +113,7 @@ pub struct RawRecipeData {
     pub craft_time: f32,
 
     /// The conditions that must be met to craft the recipe.
-    pub conditions: RecipeConditions,
+    pub conditions: Option<RecipeConditions>,
 
     /// The amount of [`Energy`] produced by making this recipe, if any.
     ///
@@ -134,7 +134,7 @@ impl From<RawRecipeData> for RecipeData {
                 .collect(),
             outputs: RecipeOutput::from_raw(raw.outputs),
             craft_time: Duration::from_secs_f32(raw.craft_time),
-            conditions: raw.conditions,
+            conditions: raw.conditions.unwrap_or_default(),
             energy: raw.energy,
         }
     }
@@ -193,7 +193,7 @@ impl RecipeData {
 
         let duration_str = format!("{:.2}", self.craft_time.as_secs_f32());
 
-        let condition_str = if self.conditions == RecipeConditions::NONE {
+        let condition_str = if self.conditions == RecipeConditions::default() {
             String::new()
         } else {
             format!("\nwhen {}", self.conditions)
@@ -204,7 +204,7 @@ impl RecipeData {
 }
 
 /// The environmental conditions needed for work to be done on a recipe.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
 pub struct RecipeConditions {
     /// The number of workers required to advance this recipe.
     pub workers_required: u8,
@@ -225,12 +225,6 @@ impl Display for RecipeConditions {
 }
 
 impl RecipeConditions {
-    /// No special conditions are needed for this recipe.
-    pub const NONE: RecipeConditions = RecipeConditions {
-        workers_required: 0,
-        allowable_light_range: None,
-    };
-
     /// Creates a new [`RecipeConditions`].
     pub const fn new(workers_required: u8, allowable_light_range: Threshold<Illuminance>) -> Self {
         Self {
diff --git a/emergence_lib/src/structures/construction.rs b/emergence_lib/src/structures/construction.rs
index 911a6ba..85094f6 100644
--- a/emergence_lib/src/structures/construction.rs
+++ b/emergence_lib/src/structures/construction.rs
@@ -355,12 +355,18 @@ impl<'w, 's> DemolitionQuery<'w, 's> {
 /// The set of tiles taken up by a structure.
 ///
 /// Structures are always "centered" on 0, 0, so these coordinates are relative to that.
-#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Footprint {
     /// The set of tiles is taken up by this structure.
     pub(crate) set: HashSet<TilePos>,
 }
 
+impl Default for Footprint {
+    fn default() -> Self {
+        Self::single()
+    }
+}
+
 impl Footprint {
     /// A footprint that occupies a single tile.
     pub fn single() -> Self {
diff --git a/emergence_lib/src/structures/structure_manifest.rs b/emergence_lib/src/structures/structure_manifest.rs
index 30f4cd9..504e153 100644
--- a/emergence_lib/src/structures/structure_manifest.rs
+++ b/emergence_lib/src/structures/structure_manifest.rs
@@ -67,7 +67,7 @@ pub struct RawStructureData {
     /// The maximum number of workers that can work at this structure at once.
     pub max_workers: u8,
     /// The tiles taken up by this building.
-    pub footprint: Footprint,
+    pub footprint: Option<Footprint>,
 }
 
 impl From<RawStructureData> for StructureData {
@@ -77,7 +77,7 @@ impl From<RawStructureData> for StructureData {
             kind: raw.kind.into(),
             construction_strategy: raw.construction_strategy.into(),
             max_workers: raw.max_workers,
-            footprint: raw.footprint,
+            footprint: raw.footprint.unwrap_or_default(),
         }
     }
 }


Test patch:
diff --git a/emergence_lib/tests/serde_manifests.rs b/emergence_lib/tests/serde_manifests.rs
index a4772df..3610270 100644
--- a/emergence_lib/tests/serde_manifests.rs
+++ b/emergence_lib/tests/serde_manifests.rs
@@ -138,10 +138,10 @@ fn can_serialize_recipe_manifest() {
                         ("acacia_seed".to_string(), 0.1),
                     ]),
                     craft_time: 3.,
-                    conditions: RecipeConditions::new(
+                    conditions: Some(RecipeConditions::new(
                         0,
                         Threshold::new(Illuminance(5e3), Illuminance(6e4)),
-                    ),
+                    )),
                     energy: Some(Energy(20.)),
                 },
             ),
@@ -151,7 +151,7 @@ fn can_serialize_recipe_manifest() {
                     inputs: HashMap::from_iter([("acacia_leaf".to_string(), 1)]),
                     outputs: HashMap::from_iter([("leuco_chunk".to_string(), 1.)]),
                     craft_time: 2.,
-                    conditions: RecipeConditions::NONE,
+                    conditions: None,
                     energy: Some(Energy(40.)),
                 },
             ),
@@ -161,10 +161,10 @@ fn can_serialize_recipe_manifest() {
                     inputs: HashMap::from_iter([("leuco_chunk".to_string(), 1)]),
                     outputs: HashMap::from_iter([("ant_egg".to_string(), 1.)]),
                     craft_time: 10.,
-                    conditions: RecipeConditions {
+                    conditions: Some(RecipeConditions {
                         workers_required: 2,
                         allowable_light_range: None,
-                    },
+                    }),
                     energy: None,
                 },
             ),
@@ -174,10 +174,10 @@ fn can_serialize_recipe_manifest() {
                     inputs: HashMap::from_iter([("ant_egg".to_string(), 1)]),
                     outputs: HashMap::new(),
                     craft_time: 10.,
-                    conditions: RecipeConditions {
+                    conditions: Some(RecipeConditions {
                         workers_required: 1,
                         allowable_light_range: None,
-                    },
+                    }),
                     energy: None,
                 },
             ),
@@ -220,7 +220,7 @@ fn can_serialize_structure_manifest() {
                         ]),
                     },
                     max_workers: 6,
-                    footprint: Footprint::single(),
+                    footprint: Some(Footprint::single()),
                 },
             ),
             (
@@ -247,7 +247,7 @@ fn can_serialize_structure_manifest() {
                         ]),
                     },
                     max_workers: 1,
-                    footprint: Footprint::single(),
+                    footprint: Some(Footprint::single()),
                 },
             ),
             (
@@ -269,7 +269,7 @@ fn can_serialize_structure_manifest() {
                         "acacia_seedling".to_string(),
                     ),
                     max_workers: 1,
-                    footprint: Footprint::single(),
+                    footprint: Some(Footprint::single()),
                 },
             ),
             (
@@ -287,7 +287,7 @@ fn can_serialize_structure_manifest() {
                         "acacia_seedling".to_string(),
                     ),
                     max_workers: 6,
-                    footprint: Footprint::single(),
+                    footprint: Some(Footprint::single()),
                 },
             ),
             (
@@ -307,7 +307,7 @@ fn can_serialize_structure_manifest() {
                         ]),
                     },
                     max_workers: 3,
-                    footprint: Footprint::hexagon(1),
+                    footprint: Some(Footprint::hexagon(1)),
                 },
             ),
             (
@@ -328,7 +328,7 @@ fn can_serialize_structure_manifest() {
                     },
                     max_workers: 6,
                     // Forms a crescent shape
-                    footprint: Footprint::single(),
+                    footprint: Some(Footprint::single()),
                 },
             ),
             (
@@ -349,7 +349,7 @@ fn can_serialize_structure_manifest() {
                         ]),
                     },
                     max_workers: 6,
-                    footprint: Footprint::single(),
+                    footprint: Some(Footprint::single()),
                 },
             ),
         ]),