#ifndef FLORA_H
#define FLORA_H
#ifndef FWORLD_H
# error Include fworld.h before flora.h so the world can be referenced.
#endif
#include <cmath>
#include <random>
#include <vector>
namespace flora {
// The largest collision radius to be considered nil.
static const double collision_epsilon = 0.000001;
// FixEntityTile constants.
static const bool
PLACE = true,
REMOVE = false;
struct Plant {
size_t entity_index;
double fertility;
};
/** Fix the tile at an entity's location.
*
* To be called when you want to place or remove an entity.
*
* When you place an entity, this function recalculates the tile at the
* entity's location. When you remove an entity, this function deletes
* the entity from the world's `entities` list and recalculates the tile
* at the entity's former location.
*/
void FixEntityTile(
size_t entity_index,
fworld::World* world,
bool action ){
auto &ent = world->entities[entity_index];
long long
x = ent.x + 0.5,
y = ent.y + 0.5;
if( action == PLACE ){
// If the plant has collisions, recalculate the map's collision
// data. Note that this is a costly operation, especially within
// a loop. Because plants reproduce by default, you may soon
// have growth cycles where collision data is recalculated
// hundreds of times.
if( std::abs( ent.collisionRadius ) > collision_epsilon ){
world->recalculateMapEntities();
}else{
// No collisions? Then it's an F0.
world->mapEntities[y][x] = 0xF0;
}
}else{ // action == REMOVE
// Delete the entity.
world->removeEntity( entity_index );
}
// The map is now on a slippery slope with regards to memory.
// This flag signals that the memory has changed and caution should
// be exercised.
world->mapChanged = true;
}
/** Plant an entity at (x,y) and add it to the list of growing plants.
*
* You can construct any `fworld::Entity` and place it using this
* function. From this point onwards, it will be treated as a plant.
*
* @return `true` on success or `false` on failure.
*/
bool Germinate(
std::vector<Plant>* plants,
fworld::World* world,
const fworld::Entity &ent,
unsigned int rand_seed,
double fertility,
long long x,
long long y ){
// Fail if the tile is blocked or filled by another static entity.
if( world->tileBlocking( x, y, true )
|| world->mapEntities[y][x] >= 0xB0 ){
return false;
}
std::mt19937_64 mt( rand_seed );
std::uniform_real_distribution<double> unit_dist( 0.0, 1.0 );
Plant p = {};
p.entity_index = world->entities.size();
p.fertility = fertility;
plants->push_back( p );
// TODO(fluffrabbit): The plant's inventory.
world->entities.push_back( ent );
auto &new_ent = world->entities.back();
new_ent.type = "flora";
new_ent.x = x;
new_ent.y = y;
// Randomly age the plant to make its growth unpredictable.
new_ent.age = unit_dist( mt ) * 0.5;
new_ent.frame = 0.0;
new_ent.animationMode = fworld::ANIMATION_MANUAL;
FixEntityTile( p.entity_index, world, PLACE );
return true;
}
/** Kill the plant located at `plant_index` in `plants`.
*/
void Kill(
size_t plant_index,
std::vector<Plant>* plants,
fworld::World* world ){
size_t entity_index = (*plants)[plant_index].entity_index;
FixEntityTile( entity_index, world, REMOVE );
plants->erase( plants->begin() + plant_index );
// Fix references to entities.
for( auto &p : *plants ){
// If plant's entity index would be affected by a deletion...
if( p.entity_index > entity_index ){
// Lower the plant's entity index by 1.
p.entity_index--;
}
}
}
/** Wrapper for `Kill()` that takes an entity index.
*/
void KillEntity(
size_t entity_index,
std::vector<Plant>* plants,
fworld::World* world ){
// Find the entity.
for( size_t i = 0; i < plants->size(); i++ ){
if( (*plants)[i].entity_index == entity_index ){
// Entity indices match.
Kill( i, plants, world );
return;
}
}
}
/** Run the growth cycles for the plants.
*/
void Grow(
std::vector<Plant>* plants,
fworld::World* world,
unsigned int rand_seed,
double growth_rate,
double fertility_rate = 1.444 ){ // 1.444 is usually pretty balanced.
// The germination probabilities match a standard 3x3 blur kernel.
// This should result in a roughly circular spread.
const double
prob_centered = fertility_rate * 0.25,
prob_cardinal = fertility_rate * 0.125,
prob_diagonal = fertility_rate * 0.0625;
std::mt19937_64 mt( rand_seed );
std::uniform_real_distribution<double> unit_dist( 0.0, 1.0 );
auto Seed = [&](
size_t plant_index,
long long x,
long long y ){
Plant &p = (*plants)[plant_index];
auto &ent = world->entities[p.entity_index];
// Round the parent plant's position to the nearest tile.
long long
e_x = ent.x + 0.5,
e_y = ent.y + 0.5;
// Determine the probability of germinating based on orientation
// relative to the parent plant.
double probability = prob_cardinal;
if( e_x != x && e_y != y ){
probability = prob_diagonal;
}else if( e_x == x && e_y == y ){
probability = prob_centered;
}
// There is a chance that the new plant will germinate at (x,y).
// Determining factors include orientation, reachability, and
// the parent plant's fertility.
if( unit_dist( mt ) < probability * p.fertility
&& world->reachable( e_x, e_y, x, y, 3, true ) ){
// If the parent plant germinates on its own tile, reset it.
if( e_x == x && e_y == y ){
// Same random aging as in Germinate.
ent.age = unit_dist( mt ) * 0.5;
ent.frame = 0.0;
}else{
Germinate( plants, world, ent, rand_seed, p.fertility, x, y );
}
}
};
// Only iterate over the initial number of plants at the beginning
// of the growth cycle.
size_t max_cycle = plants->size();
for( size_t i = 0; i < max_cycle; i++ ){
Plant &p = (*plants)[i];
auto &ent = world->entities[p.entity_index];
ent.age += growth_rate;
ent.frame = std::floor( ent.age );
// If the a is at least 3.0 (fourth frame) the plant seeds.
if( ent.age >= 3.0 && p.fertility > 0.0 ){
// Round the parent plant's position to the nearest tile.
// Yes, this is redundant.
long long
e_x = ent.x + 0.5,
e_y = ent.y + 0.5;
// Seed in the 3x3 square centered at the parent plant.
for( long long x = e_x - 1; x <= e_x + 1; x++ ){
for( long long y = e_y - 1; y <= e_y + 1; y++ ){
Seed( i, x, y );
}
}
// If the plant did not reseed itself, it is now infertile.
if( ent.age >= 3.0 ){
p.fertility = 0.0;
}
}
}
// Delete the old plants.
for( size_t i = 0; i < plants->size(); i++ ){
Plant &p = (*plants)[i];
auto &ent = world->entities[p.entity_index];
// No plants live past this age.
if( ent.age >= 4.0 ){
Kill( i, plants, world );
}
}
}
/** Returns a vector of `Plant`s in a given world.
*
* Call this function whenever a new map is loaded or an entity is
* deleted externally.
*/
std::vector<Plant> GetPlants( fworld::World* world ){
std::vector<Plant> plants;
for( size_t i = 0; i < world->entities.size(); i++ ){
auto &ent = world->entities[i];
if( ent.type == "flora" ){
ent.animationMode = fworld::ANIMATION_MANUAL;
Plant p = {};
p.entity_index = i;
// Only plants in the first 3 growth phases are fertile.
p.fertility = ent.age < 3.0 ? 1.0 : 0.0;
plants.push_back( p );
}
}
return plants;
}
} // namespace flora
#endif // FLORA_H