// "Extension" functions for `Globe`. These do not use any private details of `Globe`,
// and so they are exposed on its _inherent impl_ for convenience only.
use rand::Rng;
use grid::{GridPoint2, GridPoint3, PosInOwningRoot, GridCoord};
use grid::random_column;
use super::chunk::Material;
use super::CursorMut;
use super::globe::Globe;
impl Globe {
/// Attempt to find dry land at surface level. See `find_dry_land`.
pub fn find_surface_dry_land(
&mut self,
column: GridPoint2,
min_air_cells_above: GridCoord,
max_distance_from_starting_point: GridCoord,
) -> Option<GridPoint3> {
// Use land height from world gen to approximate cell position where we might find land.
let land_height = self.gen.land_height(column);
let approx_cell_z = self.spec().approx_cell_z_from_radius(land_height);
// Augment original column with approximate z-value and pass the buck.
let pos = column.with_z(approx_cell_z);
self.find_dry_land(pos, min_air_cells_above, max_distance_from_starting_point)
}
/// Attempt to find dry land near (above or below) the given `pos`.
///
/// A land cell position will only be returned if it has at least as many
/// contiguous cells of air directly above it as specified by `min_air_cells_above`.
///
/// Returns `None` if no such cell can be found within the maximum distance given, e.g.,
/// if the highest land was below water, or our guess about where there should be land
/// exposed to air turned out to be wrong.
///
/// Note that this returns the position of the cell found containing land, not the first cell
/// above it containing air. If you are trying to find a suitable location to, e.g., spawn new
/// entities, then you probably want to use the position one above the position returned by this function.
pub fn find_dry_land(
&mut self,
start_pos: GridPoint3,
min_air_cells_above: GridCoord,
max_distance_from_starting_point: GridCoord,
) -> Option<GridPoint3> {
// Interleave searching up and down at the same time. Start the "down" search at the
// given `start_pos`, and the "up" search one above it.
let mut distance_from_start: GridCoord = 0;
let mut down_pos = start_pos;
let mut up_pos = start_pos;
up_pos.z += 1;
// Share a cursor for searching up and down.
let start_pos_in_owning_root = PosInOwningRoot::new(start_pos, self.spec().root_resolution);
let chunk_origin = self.origin_of_chunk_owning(start_pos_in_owning_root);
let mut cursor = CursorMut::new_in_chunk(self, chunk_origin);
while distance_from_start <= max_distance_from_starting_point {
'candidate_land: for hopefully_land_pos in &[down_pos, up_pos] {
if hopefully_land_pos.z < 0 {
continue;
}
// If it's not land, then we're not interested.
cursor.set_pos(*hopefully_land_pos);
cursor.ensure_chunk_present();
{
// Non-lexical lifetimes SVP.
let cell = cursor.cell().expect(
"We just ensured the chunk is present, but apparently it's not. Kaboom!",
);
if cell.material != Material::Dirt {
continue;
}
}
// Ensure minimum required air above.
let mut hopefully_air_pos = *hopefully_land_pos;
for _ in 0..min_air_cells_above {
hopefully_air_pos.z = hopefully_air_pos.z + 1;
cursor.set_pos(hopefully_air_pos);
cursor.ensure_chunk_present();
let cell = cursor.cell().expect(
"We just ensured the chunk is present, but apparently it's not. Kaboom!",
);
if cell.material != Material::Air {
continue 'candidate_land;
}
}
// Hurrah! This land cell passed the gauntlet.
return Some(*hopefully_land_pos);
}
down_pos.z -= 1;
up_pos.z += 1;
distance_from_start += 1;
}
None
}
/// Find a random cell immediately above dry land. See `find_dry_land`.
///
/// This is useful for finding a suitable point on the surface of the planet to place new entities,
/// e.g., the player character when choosing a random spawn point on the planet.
///
/// Returns `None` if no suitable cell could be found within the maximum number of attempts.
/// Each attempt begins from a new random column.
pub fn air_above_random_surface_dry_land<R: Rng>(
&mut self,
rng: &mut R,
min_air_cells_above: GridCoord,
max_distance_from_starting_point: GridCoord,
max_attempts: usize,
) -> Option<GridPoint3> {
let mut attempts_remaining = max_attempts;
while attempts_remaining > 0 {
let column = random_column(self.spec().root_resolution, rng);
let maybe_pos = self.find_surface_dry_land(
column,
min_air_cells_above,
max_distance_from_starting_point,
);
if let Some(mut pos) = maybe_pos {
// We want the air above the land we found.
pos.z += 1;
return Some(pos);
}
attempts_remaining -= 1;
}
None
}
// TODO: this is not sufficient for finding a suitable place
// to put a cell dweller; i.e. we need something that randomly
// samples positions to find a column with land at the top,
// probably by using the `Gen` to find an approximate location,
// and then working up and down at the same time to find the
// closest land to the "surface".
//
// TODO: now that `air_above_random_surface_dry_land` exists,
// track down and destroy all uses of this.
pub fn find_lowest_cell_containing(
&mut self,
column: GridPoint3,
material: Material,
) -> GridPoint3 {
// Translate into owning root, then start at bedrock.
let mut column = PosInOwningRoot::new(column, self.spec().root_resolution);
column.set_z(0);
let chunk_origin = self.origin_of_chunk_owning(column);
let mut cursor = CursorMut::new_in_chunk(self, chunk_origin);
cursor.set_pos(column.into());
loop {
// TODO: cursor doesn't guarantee you're reading authoritative data.
// Do we care about that? Do we just need to make sure that "ensure chunk"
// loads any other chunks that might be needed? But gah, then you're going to
// have a chain reaction, and load ALL chunks. Maybe it's Cursor's
// responsibility, then. TODO: think about this. :)
//
// Maybe you need a special kind of cursor. That only looks at owned cells
// and automatically updates itself whenever you set its position.
//
// TODO: "collect garbage" occasionally? Or every iteration, even.
cursor.ensure_chunk_present();
{
let pos = cursor.pos();
let cell = cursor.cell().expect(
"We just ensured the chunk is present, but apparently it's not. Kaboom!",
);
if cell.material == material {
// Yay, we found it!
return pos.into();
}
}
let new_pos = cursor.pos().with_z(cursor.pos().z + 1);
cursor.set_pos(new_pos);
}
}
}