Initial commit

This commit is contained in:
Rendo 2026-07-29 16:58:09 +05:00
commit 1119f7b0a0
4 changed files with 6424 additions and 0 deletions

389
src/main.rs Normal file
View file

@ -0,0 +1,389 @@
use bevy::{math::UVec3, prelude::*};
//vibecode
use bevy::color::palettes::css::{DARK_GRAY,ROYAL_BLUE};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(Grid::cube(16))
.insert_resource(Time::<Fixed>::from_hz(120.))
.add_systems(FixedUpdate, tick)
//Vibecode
.add_systems(Startup, setup_visuals)
.add_systems(Update, sync_visuals)
.run();
}
const MAX_FLUID_AMOUNT: u32 = 1000;
const MAX_VERTICAL_FLOW: u32 = MAX_FLUID_AMOUNT;
const MAX_HORIZONTAL_FLOW: u32 = MAX_FLUID_AMOUNT/2;
#[derive(Default,Clone,Copy)]
enum GridElement{
#[default]
Air,
Water{amount: u32},
Solid,
}
impl GridElement {
pub fn try_fill(&self, fill_amount: u32) -> Option<u32> {
return match self {
GridElement::Water { amount } => {
if *amount == MAX_FLUID_AMOUNT {
return None;
}
else if MAX_FLUID_AMOUNT - *amount < fill_amount {
return Some(MAX_FLUID_AMOUNT - *amount);
}
else {
return Some(fill_amount);
}
},
GridElement::Air => Some(fill_amount),
_ => None,
}
}
}
#[derive(Resource,Default,Clone)]
struct Grid {
size: UVec3,
data: Vec<GridElement>
}
impl Grid {
pub fn cube(side: u32) -> Self {
let mut data = Vec::new();
let uside = side as usize;
data.resize(uside*uside*uside, GridElement::Air);
Self {
size: UVec3::new(side,side,side),
data
}
}
pub fn indexify(&self, vector: UVec3) -> usize{
let clamped_vector = vector.min(self.size - UVec3::ONE);
return (clamped_vector.x + self.size.x * clamped_vector.y + self.size.x * self.size.y * clamped_vector.z) as usize;
}
pub fn deindexify(&self, index: usize) -> UVec3 {
let undex = index as u32;
return UVec3::new(
undex % self.size.x,
undex / self.size.x % self.size.y,
undex / (self.size.x*self.size.y)
);
}
/*pub fn extract(&mut self, index: usize, extract_amount: u32) -> u32 {
if index >= self.indexify(self.size) {
return 0;
}
let mut cleanup = false;
let mut extracted_amount = 0;
if let GridElement::Water { amount } = &mut self.data[index] {
if extract_amount >= *amount {
extracted_amount = amount.clone();
*amount = 0;
cleanup = true;
}
else {
*amount -= extract_amount;
extracted_amount = extract_amount;
}
}
if cleanup {
self.data[index] = GridElement::None;
}
return extracted_amount;
}
pub fn fill(&mut self, index: usize, fill_amount: u32) -> u32 {
if index >= self.indexify(self.size) {
return fill_amount;
}
if let GridElement::None = self.data[index] {
self.data[index] = GridElement::Water { amount: fill_amount }
}
else if let GridElement::Water { amount } = &mut self.data[index] {
*amount = MAX_FLUID_AMOUNT.min(*amount + fill_amount);
}
}*/
pub fn tranfer_fluid(&mut self,from: usize, to: usize, transfer_amount: u32) {
let bounds = self.indexify(self.size);
if from >= bounds || to >= bounds {
return;
}
let from_new_value;
let to_new_value;
match (self.data[to],self.data[from]) {
(GridElement::Air,GridElement::Water { amount: mut water_amount }) => {
to_new_value = Some(GridElement::Water { amount: transfer_amount });
water_amount = water_amount.checked_sub(transfer_amount).unwrap_or(0);
from_new_value = if water_amount == 0 {Some(GridElement::Air)} else {Some(GridElement::Water { amount: water_amount })};
},
(GridElement::Water { amount: mut to_amount }, GridElement::Water { amount: mut from_amount }) => {
let transfer_amount = from_amount.min(transfer_amount);
let delta = MAX_FLUID_AMOUNT - to_amount;
if delta < transfer_amount {
to_amount = MAX_FLUID_AMOUNT;
from_amount -= delta;
}
else {
to_amount += transfer_amount;
from_amount -= transfer_amount;
}
to_new_value = Some(GridElement::Water { amount: to_amount });
from_new_value = if from_amount == 0 {Some(GridElement::Air)} else {Some(GridElement::Water { amount: from_amount })};
}
_ => {return;},
}
if let Some(value) = to_new_value {
self.data[to] = value;
}
if let Some(value) = from_new_value {
self.data[from] = value;
}
}
pub fn manipulate<'a>(&'a mut self) -> GridManipulationTools<'a> {
GridManipulationTools { grid: self }
}
}
struct GridManipulationTools<'a> {
grid: &'a mut Grid
}
impl<'a> GridManipulationTools<'a> {
pub fn set_xyz(&mut self,x: u32, y: u32, z: u32, element: GridElement) {
let index = self.grid.indexify(UVec3::new(x,y,z));
self.grid.data[index] = element;
}
pub fn fill_xyz(&mut self,x1: u32, y1: u32, z1: u32, x2: u32, y2: u32, z2: u32, element: GridElement) {
if x1 > x2 || y1 > y2 || z1 > z2 {
return;
}
for x in x1..=x2 {
for y in y1..=y2 {
for z in z1..=z2 {
let index = self.grid.indexify(UVec3::new(x,y,z));
self.grid.data[index] = element.clone();
}
}
}
}
}
enum GridAction{
MoveLiquid{
from: UVec3,
to: UVec3,
amount: u32,
}
}
fn tick(mut grid: ResMut<Grid>) {
let mut stack: Vec<GridAction> = Vec::new();
for x in 0..grid.size.x {
for y in 0..grid.size.y {
for z in 0..grid.size.z {
let vector = UVec3::new(x,y,z);
let index = grid.indexify(vector);
match grid.data[index as usize] {
GridElement::Water { amount } => {
let mut transfer_amount = amount.clone();
if let Some(gravity_vector) = vector.checked_sub(UVec3::new(0,1,0)) {
if let Some(gravity_amount) = grid.data
[grid.indexify(gravity_vector)]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: gravity_vector, amount: gravity_amount });
transfer_amount -= gravity_amount;
}
}
transfer_amount = transfer_amount.min(MAX_HORIZONTAL_FLOW) / 4;
if transfer_amount > 0 {
for lookup_vector in [UVec3::new(1,0,0),UVec3::new(0,0,1)] {
if let Some(toward_vector) = vector.checked_add(lookup_vector) {
if let Some(toward_amount) = grid.data
[grid.indexify(toward_vector)]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount });
}
}
if let Some(toward_vector) = vector.checked_sub(lookup_vector) {
if let Some(toward_amount) = grid.data
[grid.indexify(toward_vector)]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount });
}
}
}
}
},
_ => {},
}
}
}
}
for action in stack {
match action {
GridAction::MoveLiquid { from, to, amount } => {
let (from_index, to_index) = (grid.indexify(from), grid.indexify(to));
grid.tranfer_fluid(from_index, to_index, amount);
},
}
}
}
//Vibe code
const VISUAL_LEVELS: usize = 8;
/// Materials bucketed by fill level, plus a material for solid blocks.
#[derive(Resource)]
struct GridMaterials {
water_levels: Vec<Handle<StandardMaterial>>,
solid: Handle<StandardMaterial>,
}
/// Marker for a per-cell visual cube.
#[derive(Component)]
struct CellVisual;
/// Maps grid index -> spawned entity, so `sync_visuals` can update in O(1)
/// per cell without searching.
#[derive(Resource)]
struct CellEntities(Vec<Entity>);
fn setup_visuals(
mut commands: Commands,
mut grid: ResMut<Grid>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mut builder = grid.manipulate();
builder.fill_xyz(0, 0, 0, 6, 6, 6, GridElement::Solid);
builder.fill_xyz(1, 1, 1, 5, 6, 5, GridElement::Water { amount: MAX_FLUID_AMOUNT });
builder.set_xyz(6,1,3,GridElement::Air);
let center = grid.size.as_vec3() * 0.5;
// Camera positioned to see the whole cube from an angle.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(
center.x + grid.size.x as f32 * 1.4,
grid.size.y as f32 * 1.8,
center.z + grid.size.z as f32 * 1.4,
)
.looking_at(center, Vec3::Y),
));
// Simple directional light.
commands.spawn((
DirectionalLight {
illuminance: 8_000.0,
shadow_maps_enabled: false,
..default()
},
Transform::from_xyz(10.0, 30.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
));
let cube_mesh = meshes.add(Cuboid::new(0.9, 0.9, 0.9));
// Pre-build a handful of translucent blue materials, one per "fullness" bucket,
// so we don't need a unique material asset per cell.
let water_levels: Vec<Handle<StandardMaterial>> = (0..VISUAL_LEVELS)
.map(|i| {
let t = i as f32 / (VISUAL_LEVELS - 1) as f32;
materials.add(StandardMaterial {
base_color: Color::from(ROYAL_BLUE).with_alpha(0.15 + 0.75 * t),
alpha_mode: AlphaMode::Blend,
..default()
})
})
.collect();
let solid = materials.add(StandardMaterial {
base_color: Color::from(DARK_GRAY),
..default()
});
// Spawn one cube per grid cell, in the same order that `Grid::indexify`
// expects (x fastest, then y, then z), so `entities[i]` lines up with
// `grid.data[i]`. All start hidden.
let mut entities = Vec::with_capacity(grid.data.len());
for z in 0..grid.size.z {
for y in 0..grid.size.y {
for x in 0..grid.size.x {
let pos = UVec3::new(x, y, z).as_vec3() + Vec3::splat(0.5);
let entity = commands
.spawn((
Mesh3d(cube_mesh.clone()),
MeshMaterial3d(water_levels[0].clone()),
Transform::from_translation(pos),
Visibility::Hidden,
CellVisual,
))
.id();
entities.push(entity);
}
}
}
commands.insert_resource(GridMaterials { water_levels, solid });
commands.insert_resource(CellEntities(entities));
}
fn sync_visuals(
grid: Res<Grid>,
grid_materials: Res<GridMaterials>,
cell_entities: Res<CellEntities>,
mut query: Query<(&mut Visibility, &mut MeshMaterial3d<StandardMaterial>), With<CellVisual>>,
) {
// Grid is a plain Resource (not ResMut here), so `is_changed` only
// fires on frames where `tick` actually ran and mutated it.
if !grid.is_changed() {
return;
}
for (index, element) in grid.data.iter().enumerate() {
let entity = cell_entities.0[index];
let Ok((mut visibility, mut material)) = query.get_mut(entity) else {
continue;
};
match element {
GridElement::Water { amount } => {
*visibility = Visibility::Visible;
let level = ((*amount as u64 * (VISUAL_LEVELS as u64 - 1))
/ MAX_FLUID_AMOUNT as u64) as usize;
material.0 = grid_materials.water_levels[level].clone();
}
GridElement::Solid => {
*visibility = Visibility::Visible;
material.0 = grid_materials.solid.clone();
}
GridElement::Air => {
*visibility = Visibility::Hidden;
}
}
}
}