Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ bevy = { version = "0.15.3", features = [
"dynamic_linking", # REMOVE IN RELEASE
"trace",
] } # Basic game engine stuff (windows, inputs, etc.)
rand = "0.9.0"
rand = { version = "0.9.0", features = ["small_rng"] }
strum = "0.27"
strum_macros = "0.27"
bitflags = "2.9.0"

# I manually set this version because it won't work with Bevy otherwise.
uuid = "1.12.1"
num_cpus = "1.16.0"
bevy-inspector-egui = "0.29.1"
rayon = "1.10.0"
dashmap = "6.1.0"

[dev-dependencies]
insta = "1"
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod particle;
pub mod player;
pub mod render;
pub mod simulation;
pub mod utils;
pub mod world;
15 changes: 5 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@ use bevy::app::AppExit;
use bevy::input::keyboard::KeyCode;
use bevy::input::ButtonInput;
use bevy::prelude::*;
use utils::debug;
use world::camera;

mod particle;
mod player;
mod render;
mod simulation;
mod utils;
mod world;
use cavernborn::player;
use cavernborn::utils::debug;
use cavernborn::world::camera;

use crate::world::MapPlugin;
use camera::{CameraPlugin, GameCamera};
use cavernborn::world::MapPlugin;
use debug::DebugPlugin;
use player::PlayerPlugin;
use render::map_renderer::MapRendererPlugin;
use cavernborn::render::map_renderer::MapRendererPlugin;

fn main() {
App::new()
Expand Down
47 changes: 46 additions & 1 deletion src/particle/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ pub static INTERACTION_RULES: LazyLock<HashMap<InteractionPair, InteractionRule>
},
InteractionRule {
interaction_type: InteractionType::Preserve,
result: Particle::Liquid(Liquid::Water(Direction::random())),
// Note: this is evaluated once at startup, so the result must be
// a fixed particle. Randomizing state here would freeze a single
// random value for the entire run.
result: Particle::Liquid(Liquid::Water(Direction::default())),
},
);

Expand Down Expand Up @@ -78,3 +81,45 @@ pub struct InteractionRule {
pub interaction_type: InteractionType,
pub result: Particle,
}

#[cfg(test)]
mod tests {
use super::*;
use crate::particle::Direction;

#[test]
fn interaction_lookup_is_commutative() {
let water = Particle::Liquid(Liquid::Water(Direction::Still));
let lava = Particle::Liquid(Liquid::Lava(Direction::Still));

assert!(INTERACTION_RULES.contains_key(&InteractionPair {
source: water,
target: lava
}));
assert!(INTERACTION_RULES.contains_key(&InteractionPair {
source: lava,
target: water
}));
}

#[test]
fn interaction_lookup_ignores_liquid_direction() {
let water = Particle::Liquid(Liquid::Water(Direction::Left));
let lava = Particle::Liquid(Liquid::Lava(Direction::Right));

assert!(INTERACTION_RULES.contains_key(&InteractionPair {
source: water,
target: lava
}));
}

#[test]
fn unrelated_particles_have_no_rule() {
let water = Particle::Liquid(Liquid::Water(Direction::Still));

assert!(!INTERACTION_RULES.contains_key(&InteractionPair {
source: water,
target: water
}));
}
}
19 changes: 19 additions & 0 deletions src/particle/liquid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,22 @@ impl WorldGenType for Liquid {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Equality (and hashing) intentionally ignore flow direction so that rule
/// lookups treat all waters alike. Pattern matching still sees direction.
#[test]
fn liquids_compare_ignoring_direction() {
assert_eq!(
Liquid::Water(Direction::Left),
Liquid::Water(Direction::Right)
);
assert_ne!(
Liquid::Water(Direction::Still),
Liquid::Lava(Direction::Still)
);
}
}
13 changes: 0 additions & 13 deletions src/particle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,6 @@ impl Special {
Special::Gem(gem) => gem.spawn_chance(),
}
}

pub fn all_variants() -> Vec<Special> {
Special::iter().collect()
}
}

impl From<Common> for Particle {
Expand Down Expand Up @@ -200,13 +196,4 @@ impl Direction {
pub fn as_int(self) -> i32 {
self as i32
}

/// Returns a random direction.
pub fn random() -> Direction {
if rand::random() {
Direction::Left
} else {
Direction::Right
}
}
}
107 changes: 31 additions & 76 deletions src/simulation/fluid.rs
Original file line number Diff line number Diff line change
@@ -1,86 +1,41 @@
use bevy::math::UVec2;
use rand::{rngs::SmallRng, Rng};

use crate::{
particle::Liquid,
utils::coords::chunk_local_to_world,
world::chunk::ParticleMove,
};
use crate::particle::{Liquid, Particle};

use super::{handle_particle_movement, try_move, MoveResult, SimulationContext, Simulator};
use super::{try_move, Simulator, StepAction, StepContext};

pub struct FluidSimulator;

impl Simulator<Liquid> for FluidSimulator {
/// Calculates the new position for a fluid particle, reading old positions from the map and writing to new_cells.
fn simulate(
&mut self,
context: SimulationContext,
fluid: Liquid,
x: u32,
y: u32,
) -> Option<ParticleMove> {
let particle_world_pos =
chunk_local_to_world(context.original_chunk.position, UVec2::new(x, y));
let step =
self.calculate_step(&context, fluid, particle_world_pos.x, particle_world_pos.y);

match step {
MoveResult::Move(new_pos, new_particle) => {
// Source moves to new position (empty cell or Replace interaction)
handle_particle_movement(
context.original_chunk,
context.new_cells,
particle_world_pos,
new_pos,
new_particle,
false,
)
}
MoveResult::Preserve {
source_particle,
target_pos,
result,
} => {
// Source stays at its original local position
context.new_cells[x as usize][y as usize] = Some(source_particle);
// Place the interaction result at the target position
handle_particle_movement(
context.original_chunk,
context.new_cells,
particle_world_pos,
target_pos,
result,
true,
)
}
}
}
}

impl FluidSimulator {
/// Calculates the new position of a fluid particle in world coordinates.
/// It will either move to a new position, or interact with a neighboring particle if possible.
pub fn calculate_step(
/// Decides the next action for a fluid particle: fall, flow diagonally,
/// flow horizontally, or interact with a neighboring particle.
///
/// Pure with respect to the simulation state — the engine applies the
/// returned action.
fn calculate_step(
&self,
context: &SimulationContext,
context: &StepContext,
fluid: Liquid,
x: u32,
y: u32,
) -> MoveResult {
let particle = fluid.into();
world_pos: UVec2,
rng: &mut SmallRng,
) -> StepAction {
let particle: Particle = fluid.into();
let (x, y) = (world_pos.x, world_pos.y);
let buoyancy = Liquid::BUOYANCY;
let viscosity = fluid.get_viscosity();

// Try vertical movement first
for offset in (0..viscosity).rev() {
// Try vertical movement, farthest reach first.
// (Offset 0 would be the particle's own cell, so start at 1.)
for offset in (1..viscosity).rev() {
let new_pos = UVec2::new(x, (y as i32 + buoyancy * offset).max(0) as u32);
if let Some(result) = try_move(context, new_pos, particle) {
return result;
if let Some(action) = try_move(context, new_pos, particle) {
return action;
}
}

// Try diagonal movement
for offset in (0..viscosity).rev() {
// Try diagonal movement, farthest reach first.
for offset in (1..viscosity).rev() {
let new_y = (y as i32 + buoyancy).max(0) as u32;
let new_x_right = (x as i32 + offset * buoyancy).max(0) as u32;
let new_x_left = (x as i32 - offset * buoyancy).max(0) as u32;
Expand All @@ -90,21 +45,21 @@ impl FluidSimulator {

match (move_right, move_left) {
// If both are possible, choose one randomly.
(Some(right), Some(left)) => return if rand::random() { right } else { left },
// If one is possible, return that.
(Some(result), None) | (None, Some(result)) => return result,
// If neither are possible, do nothing.
(Some(right), Some(left)) => return if rng.random() { right } else { left },
// If one is possible, take that one.
(Some(action), None) | (None, Some(action)) => return action,
// If neither is possible, try a shorter reach.
(None, None) => {}
}
}

// Try moving horizontally
// Try moving horizontally along the current flow direction.
let new_x = (x as i32 + fluid.get_direction().as_int()).max(0) as u32;
if let Some(result) = try_move(context, UVec2::new(new_x, y), particle) {
return result;
if let Some(action) = try_move(context, UVec2::new(new_x, y), particle) {
return action;
}

// If no movement is possible, flip direction
MoveResult::Move(UVec2::new(x, y), fluid.get_flipped_direction().into())
// Nowhere to go: stay put and flip direction for next tick.
StepAction::Stay(fluid.get_flipped_direction().into())
}
}
Loading