From 2a8ae057716836e5e3231b0d6b4ed2e5ba7acff5 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 6 Nov 2022 19:38:22 -0500 Subject: [PATCH] Make plants actually photosynthesize (#88) * Use a single source of truth for map size * Refactor terrain module * Store map geometry in a dedicated resource, rather than using config * Refactor plugin structure to make app easier to test * Set up basic integration tests * Enable photosynthesis * Allow users to configure terrain type distribution during config * Clippy --- Cargo.toml | 2 +- emergence_game/src/main.rs | 11 +- emergence_lib/Cargo.toml | 4 + emergence_lib/src/enum_iter.rs | 67 ++++++++++++ emergence_lib/src/lib.rs | 112 ++++++++++++--------- emergence_lib/src/organisms/pathfinding.rs | 4 +- emergence_lib/src/organisms/structures.rs | 53 ++++++---- emergence_lib/src/organisms/units.rs | 8 +- emergence_lib/src/signals/configs.rs | 2 +- emergence_lib/src/terrain/generation.rs | 83 ++++++++++----- emergence_lib/src/terrain/mod.rs | 106 ++++++------------- emergence_lib/src/terrain/terrain_types.rs | 94 +++++++++++++++++ emergence_lib/src/tiles/mod.rs | 18 +--- emergence_lib/src/tiles/terrain.rs | 2 +- emergence_lib/tests/structure_viability.rs | 46 +++++++++ emergence_lib/tests/test_setup.rs | 33 ++++++ emergence_macros/src/iterable_enum.rs | 2 +- 17 files changed, 447 insertions(+), 200 deletions(-) create mode 100644 emergence_lib/src/enum_iter.rs create mode 100644 emergence_lib/src/terrain/terrain_types.rs create mode 100644 emergence_lib/tests/structure_viability.rs create mode 100644 emergence_lib/tests/test_setup.rs diff --git a/Cargo.toml b/Cargo.toml index 317c6ca14..c6eadd672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,4 @@ members = [ "emergence_macros", "tools/ci" ] -default-members = ["emergence_game"] \ No newline at end of file +default-members = ["emergence_game", "emergence_lib"] \ No newline at end of file diff --git a/emergence_game/src/main.rs b/emergence_game/src/main.rs index 07505e70d..4b373ebb9 100644 --- a/emergence_game/src/main.rs +++ b/emergence_game/src/main.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use emergence_lib::*; fn main() { App::new() @@ -8,12 +7,8 @@ fn main() { ..Default::default() }) .add_plugins(DefaultPlugins) - .add_plugin(camera::CameraPlugin) - .add_plugin(cursor::CursorTilePosPlugin) - .add_plugin(hive_mind::HiveMindPlugin) - .add_plugin(terrain::generation::GenerationPlugin) - .add_plugin(organisms::structures::StructuresPlugin) - .add_plugin(organisms::units::UnitsPlugin) - .add_plugin(signals::SignalsPlugin) + .add_plugin(emergence_lib::GraphicsPlugin) + .add_plugin(emergence_lib::SimulationPlugin) + .add_plugin(emergence_lib::InteractionPlugin) .run(); } diff --git a/emergence_lib/Cargo.toml b/emergence_lib/Cargo.toml index e0742f687..45a88e6b0 100644 --- a/emergence_lib/Cargo.toml +++ b/emergence_lib/Cargo.toml @@ -17,3 +17,7 @@ indexmap = "1.9" dashmap = "5.4" leafwing-input-manager = "0.6.1" emergence_macros = { path = "../emergence_macros" } + +[dev-dependencies] +# We need headless operation in tests +bevy_ecs_tilemap = { git = "https://github.com/StarArawn/bevy_ecs_tilemap", branch = "main", default-features = false} diff --git a/emergence_lib/src/enum_iter.rs b/emergence_lib/src/enum_iter.rs new file mode 100644 index 000000000..3993c6837 --- /dev/null +++ b/emergence_lib/src/enum_iter.rs @@ -0,0 +1,67 @@ +//! A helpful trait to allow us to iterate over all variants of an enum type + +use std::marker::PhantomData; + +/// Marks an enum whose variants can be iterated over in the order they are defined. +pub trait IterableEnum: Sized { + /// The number of variants of this action type + const N_VARIANTS: usize; + + /// Iterates over the possible variants in the order they were defined. + fn variants() -> EnumIter { + EnumIter::default() + } + + /// Returns the default value for the variant stored at the provided index if it exists. + /// + /// This is mostly used internally, to enable space-efficient iteration. + fn get_at(index: usize) -> Option; + + /// Returns the position in the defining enum of the given action + fn index(&self) -> usize; +} + +/// An iterator of enum variants. +/// +/// Created by calling [`IterableEnum::variants`]. +#[derive(Debug, Clone)] +pub struct EnumIter { + /// Keeps track of which variant should be provided next. + /// + /// Alternatively, `min(index - 1, 0)` counts how many variants have already been iterated + /// through. + index: usize, + /// Marker used to keep track of which `IterableEnum` this `EnumIter` iterates through. + /// + /// For more information, see [`PhantomData`](std::marker::PhantomData). + _phantom: PhantomData, +} + +impl Iterator for EnumIter { + type Item = A; + + fn next(&mut self) -> Option { + let item = A::get_at(self.index); + if item.is_some() { + self.index += 1; + } + + item + } +} + +impl ExactSizeIterator for EnumIter { + fn len(&self) -> usize { + A::N_VARIANTS + } +} + +// We can't derive this, because otherwise it won't work when A is not default +impl Default for EnumIter { + fn default() -> Self { + EnumIter { + index: 0, + _phantom: PhantomData::default(), + } + } +} diff --git a/emergence_lib/src/lib.rs b/emergence_lib/src/lib.rs index f13306001..4f80867c5 100644 --- a/emergence_lib/src/lib.rs +++ b/emergence_lib/src/lib.rs @@ -3,77 +3,93 @@ #![deny(clippy::missing_docs_in_private_items)] #![forbid(unsafe_code)] #![warn(clippy::doc_markdown)] -use std::marker::PhantomData; + +use bevy::app::{App, Plugin}; pub mod camera; pub mod cursor; pub mod curves; +pub mod enum_iter; pub mod hive_mind; pub mod organisms; pub mod signals; pub mod terrain; pub mod tiles; -/// Marks an enum whose variants can be iterated over in the order they are defined. -pub trait IterableEnum: Sized { - /// The number of variants of this action type - const N_VARIANTS: usize; +/// All of the code needed to make the simulation run +pub struct SimulationPlugin; - /// Iterates over the possible variants in the order they were defined. - fn variants() -> EnumIter { - EnumIter::default() +impl Plugin for SimulationPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(terrain::generation::GenerationPlugin) + .add_plugin(organisms::structures::StructuresPlugin) + .add_plugin(organisms::units::UnitsPlugin) + .add_plugin(signals::SignalsPlugin); } +} - /// Returns the default value for the variant stored at the provided index if it exists. - /// - /// This is mostly used internally, to enable space-efficient iteration. - fn get_at(index: usize) -> Option; +/// All of the code needed for users to interact with the simulation. +pub struct InteractionPlugin; - /// Returns the position in the defining enum of the given action - fn index(&self) -> usize; +impl Plugin for InteractionPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(camera::CameraPlugin) + .add_plugin(cursor::CursorTilePosPlugin) + .add_plugin(hive_mind::HiveMindPlugin); + } } -/// An iterator of enum variants. -/// -/// Created by calling [`IterableEnum::variants`]. -#[derive(Debug, Clone)] -pub struct EnumIter { - /// Keeps track of which variant should be provided next. - /// - /// Alternatively, `min(index - 1, 0)` counts how many variants have already been iterated - /// through. - index: usize, - /// Marker used to keep track of which `IterableEnum` this `EnumIter` iterates through. - /// - /// For more information, see [`PhantomData`](std::marker::PhantomData). - _phantom: PhantomData, +/// All of the code needed to draw things on screen. +pub struct GraphicsPlugin; + +impl Plugin for GraphicsPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(bevy_ecs_tilemap::TilemapPlugin); + } } -impl Iterator for EnumIter { - type Item = A; +/// Various app configurations, used for testing. +/// +/// Importing between files shared in the `tests` directory appears to be broken with this workspace config? +/// Followed directions from +pub mod testing { + use bevy::prelude::*; - fn next(&mut self) -> Option { - let item = A::get_at(self.index); - if item.is_some() { - self.index += 1; - } + /// Just [`MinimalPlugins`]. + pub fn minimal_app() -> App { + let mut app = App::new(); - item + app.add_plugins(MinimalPlugins); + + app } -} -impl ExactSizeIterator for EnumIter { - fn len(&self) -> usize { - A::N_VARIANTS + /// The [`bevy`] plugins needed to make simulation work + pub fn bevy_app() -> App { + let mut app = minimal_app(); + app.insert_resource(bevy::render::settings::WgpuSettings { + backends: None, + ..default() + }); + + app.add_plugin(bevy::asset::AssetPlugin) + .add_plugin(bevy::window::WindowPlugin) + .add_plugin(bevy::render::RenderPlugin); + app + } + + /// Just the game logic and simulation + pub fn simulation_app() -> App { + let mut app = bevy_app(); + app.add_plugin(super::SimulationPlugin); + app } -} -// We can't derive this, because otherwise it won't work when A is not default -impl Default for EnumIter { - fn default() -> Self { - EnumIter { - index: 0, - _phantom: PhantomData::default(), - } + /// Test users interacting with the app + pub fn interaction_app() -> App { + let mut app = simulation_app(); + app.add_plugin(bevy::input::InputPlugin) + .add_plugin(super::InteractionPlugin); + app } } diff --git a/emergence_lib/src/organisms/pathfinding.rs b/emergence_lib/src/organisms/pathfinding.rs index be93c2e97..9ef2e1707 100644 --- a/emergence_lib/src/organisms/pathfinding.rs +++ b/emergence_lib/src/organisms/pathfinding.rs @@ -1,6 +1,6 @@ //! Utilities to support organism pathfinding. use crate::signals::tile_signals::TileSignals; -use crate::terrain::ImpassableTerrain; +use crate::terrain::terrain_types::ImpassableTerrain; use crate::tiles::organisms::OrganismStorageItem; use crate::tiles::position::HexNeighbors; use crate::tiles::terrain::TerrainStorageItem; @@ -8,7 +8,7 @@ use bevy::prelude::*; use bevy_ecs_tilemap::map::TilemapSize; use bevy_ecs_tilemap::tiles::{TilePos, TileStorage}; use rand::distributions::WeightedError; -use rand::prelude::SliceRandom; +use rand::seq::SliceRandom; use rand::{thread_rng, Rng}; /// Select a passable, adjacent neighboring tile at random. diff --git a/emergence_lib/src/organisms/structures.rs b/emergence_lib/src/organisms/structures.rs index efd0d0028..737358f3c 100644 --- a/emergence_lib/src/organisms/structures.rs +++ b/emergence_lib/src/organisms/structures.rs @@ -4,23 +4,12 @@ //! but they can also be used for defense, research, reproduction, storage and more exotic effects. use crate::organisms::{Composition, OrganismBundle, OrganismType}; -use crate::terrain::ImpassableTerrain; +use crate::terrain::terrain_types::ImpassableTerrain; use crate::tiles::IntoTileBundle; use bevy::prelude::*; use bevy_ecs_tilemap::map::TilemapId; use bevy_ecs_tilemap::tiles::{TileBundle, TilePos}; -use config::*; -/// Common structure constants -mod config { - /// The initial mass of spawned structures - pub const STRUCTURE_STARTING_MASS: f32 = 0.5; - /// The mass at which structures will despawn - pub const STRUCTURE_DESPAWN_MASS: f32 = 0.01; - /// The upkeeep cost of each structure - pub const STRUCTURE_UPKEEP_RATE: f32 = 0.1; -} - /// The data needed to build a structure #[derive(Bundle, Default)] pub struct StructureBundle { @@ -32,7 +21,6 @@ pub struct StructureBundle { } /// All structures must pay a cost to keep themselves alive -// TODO: replace with better defaults #[derive(Component, Clone)] pub struct Structure { /// Mass cost per tick to stay alive. @@ -41,22 +29,44 @@ pub struct Structure { despawn_mass: f32, } +impl Structure { + /// The initial mass of spawned structures + pub const STARTING_MASS: f32 = 0.5; + /// The mass at which structures will despawn + pub const DESPAWN_MASS: f32 = 0.01; + /// The upkeep cost of each structure, relative to its total mass + pub const UPKEEP_RATE: f32 = 0.1; +} + impl Default for Structure { fn default() -> Self { Structure { - upkeep_rate: STRUCTURE_UPKEEP_RATE, - despawn_mass: STRUCTURE_DESPAWN_MASS, + upkeep_rate: Structure::UPKEEP_RATE, + despawn_mass: Structure::DESPAWN_MASS, } } } /// Plants can photosynthesize -#[derive(Component, Clone, Default)] +#[derive(Component, Clone)] pub struct Plant { /// Rate at which plants re-generate mass through photosynthesis. photosynthesis_rate: f32, } +impl Plant { + /// The base rate of photosynthesis + const PHOTOSYNTHESIS_RATE: f32 = 100.; +} + +impl Default for Plant { + fn default() -> Self { + Plant { + photosynthesis_rate: Plant::PHOTOSYNTHESIS_RATE, + } + } +} + /// The data needed to make a plant #[derive(Bundle, Default)] pub struct PlantBundle { @@ -80,7 +90,7 @@ impl PlantBundle { structure: Default::default(), organism_bundle: OrganismBundle { composition: Composition { - mass: STRUCTURE_STARTING_MASS, + mass: Structure::STARTING_MASS, }, ..Default::default() }, @@ -136,13 +146,20 @@ impl Plugin for StructuresPlugin { } /// Plants capture energy from the sun +/// +/// Photosynthesis scales in proportion to the surface area of plants, +/// and as a result has an allometric scaling ratio of 2. +/// +/// A plant's size (in one dimension) is considered to be proportional to the cube root of its mass. fn photosynthesize(time: Res