Skip to content
This repository has been archived by the owner on Nov 22, 2023. It is now read-only.

Commit

Permalink
Make plants actually photosynthesize (#88)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alice-i-cecile authored Nov 7, 2022
1 parent 40c5445 commit 2a8ae05
Show file tree
Hide file tree
Showing 17 changed files with 447 additions and 200 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ members = [
"emergence_macros",
"tools/ci"
]
default-members = ["emergence_game"]
default-members = ["emergence_game", "emergence_lib"]
11 changes: 3 additions & 8 deletions emergence_game/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use bevy::prelude::*;
use emergence_lib::*;

fn main() {
App::new()
Expand All @@ -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();
}
4 changes: 4 additions & 0 deletions emergence_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
67 changes: 67 additions & 0 deletions emergence_lib/src/enum_iter.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<Self>;

/// 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<A: IterableEnum> {
/// 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<A>,
}

impl<A: IterableEnum> Iterator for EnumIter<A> {
type Item = A;

fn next(&mut self) -> Option<A> {
let item = A::get_at(self.index);
if item.is_some() {
self.index += 1;
}

item
}
}

impl<A: IterableEnum> ExactSizeIterator for EnumIter<A> {
fn len(&self) -> usize {
A::N_VARIANTS
}
}

// We can't derive this, because otherwise it won't work when A is not default
impl<A: IterableEnum> Default for EnumIter<A> {
fn default() -> Self {
EnumIter {
index: 0,
_phantom: PhantomData::default(),
}
}
}
112 changes: 64 additions & 48 deletions emergence_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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<Self>;
/// 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<A: IterableEnum> {
/// 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<A>,
/// 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<A: IterableEnum> Iterator for EnumIter<A> {
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 <https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html>
pub mod testing {
use bevy::prelude::*;

fn next(&mut self) -> Option<A> {
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<A: IterableEnum> ExactSizeIterator for EnumIter<A> {
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<A: IterableEnum> Default for EnumIter<A> {
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
}
}
4 changes: 2 additions & 2 deletions emergence_lib/src/organisms/pathfinding.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! 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;
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.
Expand Down
53 changes: 35 additions & 18 deletions emergence_lib/src/organisms/structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -80,7 +90,7 @@ impl PlantBundle {
structure: Default::default(),
organism_bundle: OrganismBundle {
composition: Composition {
mass: STRUCTURE_STARTING_MASS,
mass: Structure::STARTING_MASS,
},
..Default::default()
},
Expand Down Expand Up @@ -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<Time>, mut query: Query<(&Plant, &mut Composition)>) {
for (plant, mut comp) in query.iter_mut() {
comp.mass += plant.photosynthesis_rate * time.delta_seconds() * comp.mass.powf(2.0 / 3.0);
}
}

/// All structures must pay an upkeep cost to sustain the vital functions of life
/// All structures must pay an upkeep cost to sustain the vital functions of life.
///
/// Maintenance of biological functions is proportional to the mass that must be maintained.
fn upkeep(time: Res<Time>, mut query: Query<(&Structure, &mut Composition)>) {
for (structure, mut comp) in query.iter_mut() {
comp.mass -= structure.upkeep_rate * time.delta_seconds() * comp.mass;
Expand Down
Loading

0 comments on commit 2a8ae05

Please sign in to comment.