├── .gitignore ├── .idea ├── .gitignore ├── falling-rust.iml ├── modules.xml └── vcs.xml ├── Cargo.toml ├── LICENSE.md ├── README.md ├── assets ├── icon_bucket.png ├── icon_circle.png ├── icon_element.png ├── icon_eraser.png ├── icon_move.png ├── icon_pause.png ├── icon_pencil.png ├── icon_play.png ├── icon_settings.png ├── icon_spray.png ├── icon_square.png ├── icon_step.png ├── icon_zoom_in.png └── icon_zoom_out.png ├── assets_originals └── icons.svg ├── benches └── simulation_benchmark.rs └── src ├── interface ├── fill_browser.rs ├── gui.rs ├── mod.rs ├── pointer_input.rs └── toolbox.rs ├── main.rs ├── pseudo_random.rs ├── render.rs ├── sandbox ├── cell.rs ├── element.rs └── mod.rs └── simulation.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/falling-rust.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "falling-rust" 3 | version = "0.6.0" 4 | edition = "2021" 5 | 6 | # Slow compilation and linking but minimal binary size 7 | [profile.web] 8 | inherits = "release" 9 | opt-level = "z" 10 | lto = true 11 | strip = true 12 | codegen-units = 1 13 | 14 | # Enable a small amount of optimization in debug mode 15 | [profile.dev] 16 | opt-level = 1 17 | 18 | # Enable high optimizations for dependencies but not for our code 19 | [profile.dev.package."*"] 20 | opt-level = 3 21 | 22 | [dependencies] 23 | bevy = { version = "0.12.0", default-features = false, features = [ 24 | "bevy_core_pipeline", 25 | "bevy_render", 26 | "bevy_sprite", 27 | "bevy_winit", 28 | "bevy_asset", 29 | "png", 30 | ] } 31 | bevy_egui = "0.23.0" 32 | image = { version = "0.24.7", default-features = false, features = ["png"] } 33 | 34 | [dependencies.web-sys] 35 | version = "0.3.64" 36 | features = ['Window'] 37 | 38 | [dev-dependencies] 39 | criterion = "0.5.1" 40 | 41 | [[bench]] 42 | name = "simulation_benchmark" 43 | harness = false 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2023 Fantastimaker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Falling Rust 2 | 3 | A falling-sand toy written using Rust, Bevy and egui. 4 | 5 | ## How to run 6 | 7 | A release version for Windows is available at [GitHub releases](https://github.com/grunnt/falling-rust/releases). 8 | 9 | ## How to run from the code 10 | 11 | You will need to have [Rust](https://www.rust-lang.org) installed to compile this. 12 | 13 | The simulation is quite CPU intensive, so you may want to run this in release mode: 14 | 15 | ``` 16 | cargo run --release 17 | ``` 18 | 19 | ## How to build for the web 20 | 21 | Falling-rust can be built as a WASM binary as well, which allows it to be run inside a webpage. 22 | 23 | You will need to have the `wasm32-unknown-unknown` target installed. This is easily done using rustup: 24 | ``` 25 | rustup target add wasm32-unknown-unknown 26 | ``` 27 | 28 | Then falling-rust needs to be compiled for wasm, using the profile that optimizes for binary size: 29 | ``` 30 | cargo build --profile web --target wasm32-unknown-unknown 31 | ``` 32 | 33 | And finally you can generate bindings for javascript (and an index.html page) using `wasm-bindgen`: 34 | ``` 35 | wasm-bindgen --out-dir ./wasm --target web ./target/wasm32-unknown-unknown/web/falling-rust.wasm 36 | ``` 37 | -------------------------------------------------------------------------------- /assets/icon_bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_bucket.png -------------------------------------------------------------------------------- /assets/icon_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_circle.png -------------------------------------------------------------------------------- /assets/icon_element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_element.png -------------------------------------------------------------------------------- /assets/icon_eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_eraser.png -------------------------------------------------------------------------------- /assets/icon_move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_move.png -------------------------------------------------------------------------------- /assets/icon_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_pause.png -------------------------------------------------------------------------------- /assets/icon_pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_pencil.png -------------------------------------------------------------------------------- /assets/icon_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_play.png -------------------------------------------------------------------------------- /assets/icon_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_settings.png -------------------------------------------------------------------------------- /assets/icon_spray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_spray.png -------------------------------------------------------------------------------- /assets/icon_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_square.png -------------------------------------------------------------------------------- /assets/icon_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_step.png -------------------------------------------------------------------------------- /assets/icon_zoom_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_zoom_in.png -------------------------------------------------------------------------------- /assets/icon_zoom_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grunnt/falling-rust/3a572c082a146355535627f4aebe0964911c976b/assets/icon_zoom_out.png -------------------------------------------------------------------------------- /assets_originals/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 45 | 51 | 57 | 61 | 64 | 65 | 71 | 73 | 80 | 82 | 89 | 97 | 98 | 106 | 107 | 108 | 117 | 123 | 129 | 132 | 134 | 141 | 149 | 150 | 153 | 160 | 168 | 169 | 170 | 171 | 177 | 183 | 187 | 188 | 194 | 200 | 205 | 206 | 212 | 219 | 226 | 231 | 237 | 240 | 245 | 252 | 253 | 254 | 257 | 261 | 266 | 267 | 273 | 279 | 285 | 286 | 292 | 298 | 305 | 306 | 312 | 318 | 320 | 327 | 334 | 341 | 348 | 355 | 362 | 369 | 376 | 379 | 384 | 391 | 392 | 393 | 394 | 396 | 402 | 405 | 412 | 419 | 426 | 433 | 440 | 447 | 454 | 461 | 469 | 476 | 483 | 488 | 489 | 490 | 499 | 501 | 510 | 514 | 519 | 526 | 527 | 528 | 533 | 542 | 548 | 549 | 550 | 551 | -------------------------------------------------------------------------------- /benches/simulation_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::*; 2 | use falling_rust::element::Element; 3 | use falling_rust::sandbox::SandBox; 4 | use falling_rust::simulation::{simulation_step, Simulation}; 5 | 6 | // Note: to get a meaningful benchmark avoid simulations that become static afer a number of iterations 7 | fn criterion_benchmark(criterion: &mut Criterion) { 8 | let size = 64; 9 | let mut simulation = Simulation::new(); 10 | 11 | // Empty sandbox (should be fast) 12 | let mut sandbox = SandBox::new(size, size); 13 | criterion.bench_function("empty_simulation", |b| { 14 | b.iter(|| simulation_step(&mut simulation, &mut sandbox)) 15 | }); 16 | 17 | // Water flowing from top to bottom (pretty much slowest element) 18 | let mut sandbox = SandBox::new(size, size); 19 | for x in 0..size / 4 { 20 | sandbox.set_element(x * 4, 1, Element::WaterSource, 0); 21 | } 22 | for x in 0..size / 3 { 23 | sandbox.set_element(x * 3, size - 1, Element::Drain, 0); 24 | } 25 | criterion.bench_function("water_flow_simulation", |b| { 26 | b.iter(|| simulation_step(&mut simulation, &mut sandbox)) 27 | }); 28 | 29 | // Oil and fire 30 | let mut sandbox = SandBox::new(size, size); 31 | for x in 0..size / 4 { 32 | sandbox.set_element(x * 4, 1, Element::OilSource, 0); 33 | } 34 | for x in 0..size / 3 { 35 | sandbox.set_element(x * 3, size - 1, Element::FireSource, 0); 36 | } 37 | criterion.bench_function("burning_oil_simulation", |b| { 38 | b.iter(|| simulation_step(&mut simulation, &mut sandbox)) 39 | }); 40 | } 41 | 42 | criterion_group!(benches, criterion_benchmark); 43 | criterion_main!(benches); 44 | -------------------------------------------------------------------------------- /src/interface/fill_browser.rs: -------------------------------------------------------------------------------- 1 | use bevy::{prelude::*, window::WindowResolution}; 2 | 3 | /// Plugin that matches the application window to fill the browser window. Only useable for wasm targets. 4 | pub struct FillBrowserWindowPlugin; 5 | 6 | impl Plugin for FillBrowserWindowPlugin { 7 | fn build(&self, app: &mut App) { 8 | app.add_systems(Update, browser_filler); 9 | } 10 | } 11 | 12 | fn browser_filler(mut window: Query<&mut Window>) { 13 | // Get browser window inner size 14 | let browser_window = web_sys::window().unwrap(); 15 | let browser_width = browser_window.inner_width().unwrap().as_f64().unwrap() as u32; 16 | let browser_height = browser_window.inner_height().unwrap().as_f64().unwrap() as u32; 17 | 18 | // Set it as our application window size (this will do nothing if it is the same as previous) 19 | let mut window = window.get_single_mut().unwrap(); 20 | window.resolution = WindowResolution::new(browser_width as f32, browser_height as f32); 21 | } 22 | -------------------------------------------------------------------------------- /src/interface/gui.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_egui::{ 3 | egui::{self, Color32, ColorImage, Frame, Layout, Response, style::*, TextureHandle, Ui}, 4 | EguiContexts, EguiPlugin, 5 | }; 6 | use egui::{Align2, FontId, Mesh, Pos2, Rect, Shape, Vec2}; 7 | use image::{DynamicImage, GenericImageView}; 8 | 9 | use crate::{ 10 | sandbox::*, 11 | simulation::*, 12 | spawn_sandbox, 13 | }; 14 | use crate::interface::toolbox::*; 15 | use crate::sandbox::ELEMENT_COUNT; 16 | 17 | const ICON_SIZE: f32 = 64.0; 18 | 19 | pub struct GuiPlugin; 20 | 21 | impl Plugin for GuiPlugin { 22 | fn build(&self, app: &mut App) { 23 | app.add_plugins(EguiPlugin) 24 | .add_systems(Startup, setup_gui) 25 | .add_systems(Update, gui_system); 26 | } 27 | } 28 | 29 | #[derive(Clone, Copy, PartialEq, Eq)] 30 | pub enum GuiMode { 31 | MainGui, 32 | ElementSelect, 33 | ToolSelect, 34 | SandboxSettings, 35 | MoveView, 36 | } 37 | 38 | #[derive(Resource)] 39 | pub struct SandboxGui { 40 | pub mode: GuiMode, 41 | pub last_element: Element, 42 | pub bucket_icon_handle: TextureHandle, 43 | pub icon_circle_handle: TextureHandle, 44 | pub icon_square_handle: TextureHandle, 45 | pub icon_pencil_handle: TextureHandle, 46 | pub icon_spray_handle: TextureHandle, 47 | pub icon_bucket_handle: TextureHandle, 48 | pub icon_play_handle: TextureHandle, 49 | pub icon_pause_handle: TextureHandle, 50 | pub icon_zoom_in_handle: TextureHandle, 51 | pub icon_zoom_out_handle: TextureHandle, 52 | pub icon_move_handle: TextureHandle, 53 | pub icon_settings_handle: TextureHandle, 54 | pub icon_eraser_handle: TextureHandle, 55 | pub icon_step_handle: TextureHandle, 56 | pub element_icons: [TextureHandle; ELEMENT_COUNT], 57 | } 58 | 59 | 60 | // System for initializing the gui style and generating element icons 61 | fn setup_gui(mut commands: Commands, mut egui_contexts: EguiContexts) { 62 | // General styling 63 | let mut style = egui::Style::default(); 64 | style.spacing = Spacing::default(); 65 | style.spacing.scroll_bar_width = 20.0; 66 | style.spacing.button_padding = bevy_egui::egui::Vec2::new(10.0, 10.0); 67 | egui_contexts.ctx_mut().set_style(style); 68 | 69 | // Generate element icons 70 | let background = image::load_from_memory(include_bytes!("../../assets/icon_element.png")).unwrap(); 71 | let element_icons = [ 72 | generate_element_image(Element::Air, &mut egui_contexts, &background), 73 | generate_element_image(Element::Sand, &mut egui_contexts, &background), 74 | generate_element_image(Element::Rock, &mut egui_contexts, &background), 75 | generate_element_image(Element::Water, &mut egui_contexts, &background), 76 | generate_element_image(Element::Acid, &mut egui_contexts, &background), 77 | generate_element_image(Element::Drain, &mut egui_contexts, &background), 78 | generate_element_image(Element::Wood, &mut egui_contexts, &background), 79 | generate_element_image(Element::Iron, &mut egui_contexts, &background), 80 | generate_element_image(Element::Rust, &mut egui_contexts, &background), 81 | generate_element_image(Element::Fire, &mut egui_contexts, &background), 82 | generate_element_image(Element::Ash, &mut egui_contexts, &background), 83 | generate_element_image(Element::Oil, &mut egui_contexts, &background), 84 | generate_element_image(Element::Lava, &mut egui_contexts, &background), 85 | generate_element_image(Element::Smoke, &mut egui_contexts, &background), 86 | generate_element_image(Element::Life, &mut egui_contexts, &background), 87 | generate_element_image(Element::Seed, &mut egui_contexts, &background), 88 | generate_element_image(Element::Plant, &mut egui_contexts, &background), 89 | generate_element_image(Element::TNT, &mut egui_contexts, &background), 90 | generate_element_image(Element::Gunpowder, &mut egui_contexts, &background), 91 | generate_element_image(Element::Fuse, &mut egui_contexts, &background), 92 | generate_element_image(Element::Explosion, &mut egui_contexts, &background), 93 | generate_element_image(Element::WaterSource, &mut egui_contexts, &background), 94 | generate_element_image(Element::AcidSource, &mut egui_contexts, &background), 95 | generate_element_image(Element::OilSource, &mut egui_contexts, &background), 96 | generate_element_image(Element::FireSource, &mut egui_contexts, &background), 97 | generate_element_image(Element::LavaSource, &mut egui_contexts, &background), 98 | generate_element_image(Element::Indestructible, &mut egui_contexts, &background), 99 | ]; 100 | 101 | commands.insert_resource(SandboxGui { 102 | mode: GuiMode::MainGui, 103 | last_element: Element::Sand, 104 | bucket_icon_handle: add_icon( 105 | &mut egui_contexts, 106 | "icon_bucket", 107 | include_bytes!("../../assets/icon_bucket.png"), 108 | ), 109 | icon_circle_handle: add_icon( 110 | &mut egui_contexts, 111 | "icon_circle", 112 | include_bytes!("../../assets/icon_circle.png"), 113 | ), 114 | icon_square_handle: add_icon( 115 | &mut egui_contexts, 116 | "icon_square", 117 | include_bytes!("../../assets/icon_square.png"), 118 | ), 119 | icon_pencil_handle: add_icon( 120 | &mut egui_contexts, 121 | "icon_pencil", 122 | include_bytes!("../../assets/icon_pencil.png"), 123 | ), 124 | icon_spray_handle: add_icon( 125 | &mut egui_contexts, 126 | "icon_spray", 127 | include_bytes!("../../assets/icon_spray.png"), 128 | ), 129 | icon_bucket_handle: add_icon( 130 | &mut egui_contexts, 131 | "icon_bucket", 132 | include_bytes!("../../assets/icon_bucket.png"), 133 | ), 134 | icon_play_handle: add_icon( 135 | &mut egui_contexts, 136 | "icon_play", 137 | include_bytes!("../../assets/icon_play.png"), 138 | ), 139 | icon_pause_handle: add_icon( 140 | &mut egui_contexts, 141 | "icon_pause", 142 | include_bytes!("../../assets/icon_pause.png"), 143 | ), 144 | icon_zoom_in_handle: add_icon( 145 | &mut egui_contexts, 146 | "icon_zoom_in", 147 | include_bytes!("../../assets/icon_zoom_in.png"), 148 | ), 149 | icon_zoom_out_handle: add_icon( 150 | &mut egui_contexts, 151 | "icon_zoom_out", 152 | include_bytes!("../../assets/icon_zoom_out.png"), 153 | ), 154 | icon_move_handle: add_icon( 155 | &mut egui_contexts, 156 | "icon_move", 157 | include_bytes!("../../assets/icon_move.png"), 158 | ), 159 | icon_settings_handle: add_icon( 160 | &mut egui_contexts, 161 | "icon_settings", 162 | include_bytes!("../../assets/icon_settings.png"), 163 | ), 164 | icon_eraser_handle: add_icon( 165 | &mut egui_contexts, 166 | "icon_eraser", 167 | include_bytes!("../../assets/icon_eraser.png"), 168 | ), 169 | icon_step_handle: add_icon( 170 | &mut egui_contexts, 171 | "icon_step", 172 | include_bytes!("../../assets/icon_step.png"), 173 | ), 174 | element_icons, 175 | }); 176 | } 177 | 178 | // Simple GUI for use both in desktop and touchscreen (via web) applications 179 | pub fn gui_system( 180 | mut egui_contexts: EguiContexts, 181 | mut camera: Query<&mut Transform, With>, 182 | mut gui: ResMut, 183 | mut toolbox: ResMut, 184 | mut simulation: ResMut, 185 | sandbox: Query<(Entity, &mut SandBox)>, 186 | commands: Commands, 187 | images: ResMut>, 188 | ) { 189 | right_side_toolbar( 190 | &mut egui_contexts, 191 | &mut gui, 192 | &mut simulation, 193 | camera.single_mut().as_mut(), 194 | ); 195 | 196 | bottom_toolbar(&mut egui_contexts, &mut gui, &mut toolbox); 197 | 198 | if gui.mode == GuiMode::SandboxSettings { 199 | settings_panel( 200 | &mut egui_contexts, 201 | sandbox, 202 | commands, 203 | images, 204 | &simulation, 205 | &mut gui, 206 | ); 207 | } else if gui.mode == GuiMode::ElementSelect { 208 | element_select_panel(&mut egui_contexts, &mut gui, &mut toolbox); 209 | } else if gui.mode == GuiMode::ToolSelect { 210 | side_panel_left_tool_select(egui_contexts, gui, toolbox); 211 | } 212 | } 213 | 214 | // Select a tool for world editing 215 | fn side_panel_left_tool_select( 216 | mut egui_contexts: EguiContexts, 217 | mut gui: ResMut, 218 | mut toolbox: ResMut, 219 | ) { 220 | egui::CentralPanel::default() 221 | .frame(Frame::none()) 222 | .show(egui_contexts.ctx_mut(), |ui| { 223 | ui.with_layout( 224 | Layout::from_main_dir_and_cross_align( 225 | egui::Direction::LeftToRight, 226 | egui::Align::Min, 227 | ) 228 | .with_main_wrap(true), 229 | |ui| { 230 | if ui 231 | .add( 232 | egui::widgets::ImageButton::new( 233 | &gui.icon_pencil_handle, 234 | ) 235 | .frame(false), 236 | ) 237 | .clicked() 238 | { 239 | toolbox.tool = Tool::Pixel; 240 | gui.mode = GuiMode::MainGui; 241 | }; 242 | if ui 243 | .add( 244 | egui::widgets::ImageButton::new( 245 | &gui.icon_circle_handle, 246 | ) 247 | .frame(false), 248 | ) 249 | .clicked() 250 | { 251 | toolbox.tool = Tool::Circle; 252 | gui.mode = GuiMode::MainGui; 253 | }; 254 | if ui 255 | .add( 256 | egui::widgets::ImageButton::new( 257 | &gui.icon_square_handle, 258 | ) 259 | .frame(false), 260 | ) 261 | .clicked() 262 | { 263 | toolbox.tool = Tool::Square; 264 | gui.mode = GuiMode::MainGui; 265 | }; 266 | if ui 267 | .add( 268 | egui::widgets::ImageButton::new( 269 | &gui.icon_spray_handle, 270 | ) 271 | .frame(false), 272 | ) 273 | .clicked() 274 | { 275 | toolbox.tool = Tool::Spray; 276 | gui.mode = GuiMode::MainGui; 277 | }; 278 | if ui 279 | .add( 280 | egui::widgets::ImageButton::new( 281 | &gui.icon_bucket_handle, 282 | ) 283 | .frame(false), 284 | ) 285 | .clicked() 286 | { 287 | toolbox.tool = Tool::Fill; 288 | gui.mode = GuiMode::MainGui; 289 | }; 290 | if toolbox.tool != Tool::Pixel && toolbox.tool != Tool::Fill { 291 | ui.add(egui::Slider::new(&mut toolbox.tool_size, 1..=64)); 292 | } 293 | }, 294 | ); 295 | }); 296 | } 297 | 298 | // Select an element to use in world editing 299 | fn element_select_panel( 300 | egui_contexts: &mut EguiContexts, 301 | gui: &mut ResMut, 302 | toolbox: &mut ResMut, 303 | ) { 304 | egui::CentralPanel::default() 305 | .frame(Frame::none()) 306 | .show(egui_contexts.ctx_mut(), |ui| { 307 | ui.with_layout( 308 | Layout::from_main_dir_and_cross_align( 309 | egui::Direction::LeftToRight, 310 | egui::Align::Min, 311 | ) 312 | .with_main_wrap(true), 313 | |ui| { 314 | element_button_click(ui, gui, Element::Sand, toolbox); 315 | element_button_click(ui, gui, Element::Wood, toolbox); 316 | element_button_click(ui, gui, Element::Iron, toolbox); 317 | element_button_click(ui, gui, Element::Rock, toolbox); 318 | element_button_click(ui, gui, Element::Water, toolbox); 319 | element_button_click(ui, gui, Element::Acid, toolbox); 320 | element_button_click(ui, gui, Element::Oil, toolbox); 321 | element_button_click(ui, gui, Element::Lava, toolbox); 322 | element_button_click(ui, gui, Element::Fire, toolbox); 323 | element_button_click(ui, gui, Element::Life, toolbox); 324 | element_button_click(ui, gui, Element::Seed, toolbox); 325 | element_button_click(ui, gui, Element::TNT, toolbox); 326 | element_button_click(ui, gui, Element::Gunpowder, toolbox); 327 | element_button_click(ui, gui, Element::Fuse, toolbox); 328 | element_button_click(ui, gui, Element::WaterSource, toolbox); 329 | element_button_click(ui, gui, Element::AcidSource, toolbox); 330 | element_button_click(ui, gui, Element::LavaSource, toolbox); 331 | element_button_click(ui, gui, Element::FireSource, toolbox); 332 | element_button_click(ui, gui, Element::Drain, toolbox); 333 | }, 334 | ); 335 | }); 336 | } 337 | 338 | // World settings panel 339 | fn settings_panel( 340 | egui_contexts: &mut EguiContexts, 341 | mut sandbox: Query<(Entity, &mut SandBox)>, 342 | mut commands: Commands, 343 | mut images: ResMut>, 344 | simulation: &Simulation, 345 | gui: &mut ResMut, 346 | ) { 347 | egui::SidePanel::left("settings").show(egui_contexts.ctx_mut(), |ui| { 348 | let (entity, sandbox) = sandbox.single_mut(); 349 | let mut new_sandbox_size = None; 350 | 351 | ui.label("New sandbox:"); 352 | if ui.button("Tiny (64x64)").clicked() { 353 | new_sandbox_size = Some((64, 64)); 354 | } 355 | if ui.button("Small (128x128)").clicked() { 356 | new_sandbox_size = Some((128, 128)); 357 | } 358 | if ui.button("Normal (256x256)").clicked() { 359 | new_sandbox_size = Some((256, 256)); 360 | } 361 | if ui.button("Large (512x512)").clicked() { 362 | new_sandbox_size = Some((512, 512)); 363 | } 364 | if ui.button("Huge (1024x1024)").clicked() { 365 | new_sandbox_size = Some((1024, 1024)); 366 | } 367 | 368 | if let Some((width, height)) = new_sandbox_size { 369 | commands.entity(entity).despawn(); 370 | spawn_sandbox( 371 | commands, 372 | images.as_mut(), 373 | width, 374 | height, 375 | ); 376 | gui.mode = GuiMode::MainGui; 377 | } 378 | ui.separator(); 379 | ui.label(format!( 380 | "Simulation: {} ms", 381 | simulation.frame_time_ms 382 | )); 383 | ui.label(format!( 384 | "Rendering: {} ms", 385 | sandbox.render_time_ms 386 | )); 387 | ui.separator(); 388 | ui.hyperlink_to("Made by Bas@Fantastimaker", "https://fantastimaker.nl"); 389 | ui.hyperlink_to("Using Bevy", "https://bevyengine.org"); 390 | }); 391 | } 392 | 393 | fn bottom_toolbar( 394 | egui_contexts: &mut EguiContexts, 395 | gui: &mut ResMut, 396 | toolbox: &mut ResMut, 397 | ) { 398 | egui::TopBottomPanel::bottom("bottom_panel") 399 | .frame(Frame::none()) 400 | .show_separator_line(false) 401 | .resizable(false) 402 | .show(egui_contexts.ctx_mut(), |ui| { 403 | ui.horizontal(|ui| { 404 | let eraser_button = egui::widgets::ImageButton::new( 405 | &gui.icon_eraser_handle, 406 | ) 407 | .frame(false); 408 | let eraser_button = if toolbox.element == Element::Air { 409 | eraser_button.tint(Color32::LIGHT_GREEN) 410 | } else { 411 | eraser_button 412 | }; 413 | if ui.add(eraser_button).clicked() { 414 | if toolbox.element == Element::Air { 415 | toolbox.element = gui.last_element; 416 | } else { 417 | toolbox.element = Element::Air; 418 | } 419 | }; 420 | 421 | if element_button(ui, gui, toolbox.element).clicked() { 422 | if gui.mode == GuiMode::ElementSelect { 423 | gui.mode = GuiMode::MainGui; 424 | } else { 425 | gui.mode = GuiMode::ElementSelect; 426 | } 427 | }; 428 | 429 | let tool_button = egui::widgets::ImageButton::new( 430 | match toolbox.tool { 431 | Tool::Pixel => &gui.icon_pencil_handle, 432 | Tool::Circle => &gui.icon_circle_handle, 433 | Tool::Square => &gui.icon_square_handle, 434 | Tool::Spray => &gui.icon_spray_handle, 435 | Tool::Fill => &gui.icon_bucket_handle, 436 | }, 437 | ) 438 | .frame(false); 439 | let tool_button = if gui.mode == GuiMode::ToolSelect { 440 | tool_button.tint(Color32::LIGHT_GREEN) 441 | } else { 442 | tool_button 443 | }; 444 | if ui.add(tool_button).clicked() { 445 | if gui.mode == GuiMode::ToolSelect { 446 | gui.mode = GuiMode::MainGui; 447 | } else { 448 | gui.mode = GuiMode::ToolSelect; 449 | } 450 | }; 451 | }); 452 | }); 453 | } 454 | 455 | fn right_side_toolbar( 456 | egui_contexts: &mut EguiContexts, 457 | gui: &mut ResMut, 458 | simulation: &mut ResMut, 459 | transform: &mut Transform, 460 | ) { 461 | egui::SidePanel::right("right_panel") 462 | .frame(Frame::none()) 463 | .show_separator_line(false) 464 | .resizable(false) 465 | .min_width(ICON_SIZE) 466 | .show(egui_contexts.ctx_mut(), |ui| { 467 | let settings_button = 468 | egui::widgets::ImageButton::new(&gui.icon_settings_handle) 469 | .frame(false); 470 | let settings_button = if gui.mode == GuiMode::SandboxSettings { 471 | settings_button.tint(Color32::LIGHT_GREEN) 472 | } else { 473 | settings_button 474 | }; 475 | if ui.add(settings_button).clicked() { 476 | gui.mode = if gui.mode == GuiMode::SandboxSettings { 477 | GuiMode::MainGui 478 | } else { 479 | GuiMode::SandboxSettings 480 | } 481 | }; 482 | if ui 483 | .add( 484 | egui::widgets::ImageButton::new( 485 | if simulation.running { 486 | &gui.icon_play_handle 487 | } else { 488 | &gui.icon_pause_handle 489 | }, 490 | ) 491 | .frame(false), 492 | ) 493 | .clicked() 494 | { 495 | simulation.running = !simulation.running; 496 | }; 497 | if !simulation.running { 498 | if ui 499 | .add( 500 | egui::widgets::ImageButton::new( 501 | &gui.icon_step_handle, 502 | ) 503 | .frame(false), 504 | ) 505 | .clicked() 506 | { 507 | simulation.step = true; 508 | }; 509 | } 510 | 511 | if ui 512 | .add( 513 | egui::widgets::ImageButton::new( 514 | &gui.icon_zoom_in_handle, 515 | ) 516 | .frame(false), 517 | ) 518 | .clicked() 519 | { 520 | transform.scale.x = (transform.scale.x * 0.9).clamp(0.1, 1.0); 521 | transform.scale.y = (transform.scale.y * 0.9).clamp(0.1, 1.0); 522 | }; 523 | if ui 524 | .add( 525 | egui::widgets::ImageButton::new( 526 | &gui.icon_zoom_out_handle, 527 | ) 528 | .frame(false), 529 | ) 530 | .clicked() 531 | { 532 | transform.scale.x = (transform.scale.x * 1.1).clamp(0.1, 1.0); 533 | transform.scale.y = (transform.scale.y * 1.1).clamp(0.1, 1.0); 534 | }; 535 | let move_button = 536 | egui::widgets::ImageButton::new(&gui.icon_move_handle) 537 | .frame(false); 538 | let move_button = if gui.mode == GuiMode::MoveView { 539 | move_button.tint(Color32::LIGHT_GREEN) 540 | } else { 541 | move_button 542 | }; 543 | if ui.add(move_button).clicked() { 544 | gui.mode = if gui.mode == GuiMode::MoveView { 545 | GuiMode::MainGui 546 | } else { 547 | GuiMode::MoveView 548 | } 549 | }; 550 | }); 551 | } 552 | 553 | fn add_icon(egui_contexts: &mut EguiContexts, name: &str, image_data: &[u8]) -> TextureHandle { 554 | let image = image::load_from_memory(image_data).unwrap(); 555 | let size = [image.width() as _, image.height() as _]; 556 | let image_buffer = image.to_rgba8(); 557 | let pixels = image_buffer.as_flat_samples(); 558 | 559 | let icon_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); 560 | let icon_texture_handle = 561 | egui_contexts 562 | .ctx_mut() 563 | .load_texture(name, icon_image, Default::default()); 564 | icon_texture_handle 565 | } 566 | 567 | fn element_button_click( 568 | ui: &mut Ui, 569 | gui: &mut ResMut, 570 | element: Element, 571 | toolbox: &mut ResMut, 572 | ) { 573 | if element_button(ui, gui, element).clicked() { 574 | toolbox.element = element; 575 | gui.mode = GuiMode::MainGui; 576 | } 577 | } 578 | 579 | fn element_button(ui: &mut Ui, gui: &mut SandboxGui, element: Element) -> Response { 580 | const SIZE: f32 = ICON_SIZE; 581 | let (rect, response) = ui.allocate_exact_size(Vec2::new(SIZE, SIZE), egui::Sense::click()); 582 | 583 | if ui.is_rect_visible(rect) { 584 | let mut mesh = Mesh::with_texture(gui.element_icons[element as usize].id()); 585 | mesh.add_rect_with_uv( 586 | rect, 587 | Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)), 588 | Color32::WHITE, 589 | ); 590 | ui.painter().add(Shape::mesh(mesh)); 591 | // Element name (with simple shadow) 592 | let name = element.to_string().replace(" ", "\n"); 593 | ui.painter().text( 594 | rect.left_top() + Vec2::new(11.0, 16.0), 595 | Align2::LEFT_TOP, 596 | &name, 597 | FontId::proportional(14.0), 598 | Color32::BLACK, 599 | ); 600 | ui.painter().text( 601 | rect.left_top() + Vec2::new(10.0, 15.0), 602 | Align2::LEFT_TOP, 603 | &name, 604 | FontId::proportional(14.0), 605 | Color32::WHITE, 606 | ); 607 | } 608 | response 609 | } 610 | 611 | // Create a button image for element selection 612 | pub fn generate_element_image( 613 | element: Element, 614 | egui_context: &mut EguiContexts, 615 | background: &DynamicImage, 616 | ) -> TextureHandle { 617 | // Generate a tiny sandbox containing our element 618 | let size = 64; 619 | let mut sandbox = SandBox::new(size, size); 620 | let mut toolbox = ToolBox::default(); 621 | toolbox.element = element; 622 | toolbox.tool = Tool::Square; 623 | toolbox.tool_size = size; 624 | let center = (size / 2) as isize; 625 | toolbox.apply(&mut sandbox, size / 2, size / 2); 626 | 627 | let mut img = ColorImage::new([size, size], Color32::TRANSPARENT); 628 | 629 | for y in 0..size { 630 | for x in 0..size { 631 | // Get the background image color 632 | let pixel = background.get_pixel(x as u32, y as u32); 633 | let (or, og, ob, oa) = (pixel.0[0], pixel.0[1], pixel.0[2], pixel.0[3]); 634 | 635 | // Get the element color 636 | let cell = sandbox.get_mut(x, y); 637 | let (cr, cg, cb) = element_type(cell.element).color; 638 | 639 | // Do a simplified alpha blend between the two to soften the edges 640 | let dx = (center - x as isize).abs() as f32; 641 | let dy = (center - y as isize).abs() as f32; 642 | let alpha = 1.0 - ((dx * dx + dy * dy) / (size as f32 / 2.0).powf(2.0)).powf(3.0); 643 | let r = (cr as f32 * alpha + or as f32 * (1.0 - alpha)) as u8; 644 | let g = (cg as f32 * alpha + og as f32 * (1.0 - alpha)) as u8; 645 | let b = (cb as f32 * alpha + ob as f32 * (1.0 - alpha)) as u8; 646 | img[(x, y)] = Color32::from_rgba_premultiplied(r, g, b, oa); 647 | } 648 | } 649 | 650 | egui_context.ctx_mut().load_texture( 651 | format!("element_{}", element as u8), 652 | img, 653 | Default::default(), 654 | ) 655 | } 656 | -------------------------------------------------------------------------------- /src/interface/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use crate::interface::gui::GuiPlugin; 3 | use crate::interface::pointer_input::PointerInputPlugin; 4 | use crate::interface::toolbox::ToolBox; 5 | 6 | mod fill_browser; 7 | mod gui; 8 | mod pointer_input; 9 | mod toolbox; 10 | 11 | pub struct InterfacePlugin; 12 | 13 | impl Plugin for InterfacePlugin { 14 | fn build(&self, app: &mut App) { 15 | app.add_plugins(GuiPlugin) 16 | .add_plugins(PointerInputPlugin) 17 | .init_resource::(); 18 | 19 | #[cfg(target_family = "wasm")] 20 | app.add_plugins(FillBrowserWindowPlugin); 21 | } 22 | } -------------------------------------------------------------------------------- /src/interface/pointer_input.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | input::{ 3 | ButtonState, 4 | mouse::{MouseButtonInput, MouseWheel}, 5 | }, 6 | prelude::*, 7 | render::camera::Camera, 8 | }; 9 | use bevy_egui::EguiContexts; 10 | 11 | use crate::interface::gui::{GuiMode, SandboxGui}; 12 | use crate::interface::toolbox::ToolBox; 13 | use crate::sandbox::{Element, SandBox}; 14 | 15 | /// Handles both mouse and touch input for the sandbox editor 16 | pub struct PointerInputPlugin; 17 | 18 | impl Plugin for PointerInputPlugin { 19 | fn build(&self, app: &mut App) { 20 | app.init_resource::() 21 | .add_systems(PreUpdate, pointer_input); 22 | } 23 | } 24 | 25 | #[derive(Default, Resource)] 26 | pub struct PointerInputState { 27 | pub left_button_down: bool, 28 | pub middle_button_down: bool, 29 | pub right_button_down: bool, 30 | pub position: Vec2, 31 | pub drag_movement: Vec2, 32 | pub world_position: Vec2, 33 | } 34 | 35 | pub fn pointer_input( 36 | mut mouse: ResMut, 37 | mut mouse_button_input_events: EventReader, 38 | mut cursor_moved_events: EventReader, 39 | mut mouse_wheel_events: EventReader, 40 | mut camera: Query<(&Camera, &mut Transform, &GlobalTransform)>, 41 | mut egui_context: EguiContexts, 42 | mut toolbox: ResMut, 43 | mut sandbox: Query<&mut SandBox>, 44 | gui: Res, 45 | ) { 46 | // Determine button state 47 | for event in mouse_button_input_events.read() { 48 | if event.button == MouseButton::Left { 49 | mouse.left_button_down = event.state == ButtonState::Pressed; 50 | } 51 | if event.button == MouseButton::Middle { 52 | mouse.middle_button_down = event.state == ButtonState::Pressed; 53 | } 54 | if event.button == MouseButton::Right { 55 | mouse.right_button_down = event.state == ButtonState::Pressed; 56 | } 57 | } 58 | 59 | // Record latest position 60 | let last_position = mouse.position; 61 | for event in cursor_moved_events.read() { 62 | mouse.position = event.position; 63 | } 64 | mouse.drag_movement = if mouse.left_button_down || mouse.middle_button_down { 65 | last_position - mouse.position 66 | } else { 67 | Vec2::ZERO 68 | }; 69 | 70 | // Check mouse wheel 71 | let mut wheel_y = 0.0; 72 | for event in mouse_wheel_events.read() { 73 | wheel_y += event.y; 74 | } 75 | 76 | let ctx = egui_context.ctx_mut(); 77 | if ctx.wants_pointer_input() 78 | || ctx.is_pointer_over_area() 79 | || ctx.is_using_pointer() 80 | || ctx.wants_pointer_input() 81 | { 82 | // GUI gets priority input 83 | mouse.left_button_down = false; 84 | mouse.middle_button_down = false; 85 | mouse.right_button_down = false; 86 | return; 87 | } 88 | 89 | let sandbox = sandbox.get_single_mut(); 90 | if sandbox.is_err() { 91 | // Sandbox not active 92 | return; 93 | } 94 | 95 | let mut sandbox = sandbox.unwrap(); 96 | // Update world position of the pointer (e.g. for use while editing the world) 97 | let (camera, mut transform, global_transform) = camera.single_mut(); 98 | let world_pos = camera 99 | .viewport_to_world(global_transform, mouse.position) 100 | .unwrap() 101 | .origin; 102 | mouse.world_position = Vec2::new( 103 | world_pos.x + (sandbox.width() / 2) as f32, 104 | (sandbox.height() / 2) as f32 - world_pos.y, 105 | ); 106 | 107 | // Zoom camera using mouse wheel 108 | if wheel_y > 0.0 { 109 | transform.scale.x = (transform.scale.x * 0.9).clamp(0.1, 1.0); 110 | transform.scale.y = (transform.scale.y * 0.9).clamp(0.1, 1.0); 111 | } else if wheel_y < 0.0 { 112 | transform.scale.x = (transform.scale.x * 1.1).clamp(0.1, 1.0); 113 | transform.scale.y = (transform.scale.y * 1.1).clamp(0.1, 1.0); 114 | } 115 | 116 | // Pan camera 117 | let half_width = (sandbox.width() / 2) as f32; 118 | let half_height = (sandbox.height() / 2) as f32; 119 | if mouse.middle_button_down || (gui.mode == GuiMode::MoveView && mouse.left_button_down) { 120 | transform.translation.x += mouse.drag_movement.x * transform.scale.x; 121 | transform.translation.y -= mouse.drag_movement.y * transform.scale.y; 122 | 123 | transform.translation.x = transform.translation.x.clamp(-half_width, half_width); 124 | transform.translation.y = transform.translation.y.clamp(-half_height, half_height); 125 | } 126 | 127 | // Edit the world 128 | if gui.mode != GuiMode::MoveView { 129 | let (x, y) = (mouse.world_position.x, mouse.world_position.y); 130 | if x > 0.0 && x < sandbox.width() as f32 && y > 0.0 && y < sandbox.height() as f32 { 131 | if mouse.left_button_down { 132 | toolbox.apply(&mut sandbox, x.floor() as usize, y.floor() as usize); 133 | } else if mouse.right_button_down { 134 | let element = toolbox.element; 135 | toolbox.element = Element::Air; 136 | toolbox.apply(&mut sandbox, x.floor() as usize, y.floor() as usize); 137 | toolbox.element = element; 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/interface/toolbox.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bevy::prelude::Resource; 4 | 5 | use crate::{pseudo_random::PseudoRandom, sandbox::*}; 6 | 7 | // Tools for editing the world 8 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 9 | pub enum Tool { 10 | Pixel, 11 | Circle, 12 | Square, 13 | Spray, 14 | Fill, 15 | } 16 | 17 | #[derive(Resource)] 18 | pub struct ToolBox { 19 | pub tool: Tool, 20 | pub element: Element, 21 | pub tool_size: usize, 22 | pub random: PseudoRandom, 23 | } 24 | 25 | impl ToolBox { 26 | pub fn apply(&mut self, sandbox: &mut SandBox, x: usize, y: usize) { 27 | let half_size = self.tool_size / 2; 28 | let remainder = if half_size == 0 { 29 | 1 30 | } else { 31 | self.tool_size % half_size 32 | }; 33 | let x1 = if x > half_size { x - half_size } else { 1 }; 34 | let x2 = if x + half_size + remainder < sandbox.width() { 35 | x + half_size + remainder 36 | } else { 37 | sandbox.width() 38 | }; 39 | let y1 = if y > half_size { y - half_size } else { 1 }; 40 | let y2 = if y + half_size + remainder < sandbox.height() { 41 | y + half_size + remainder 42 | } else { 43 | sandbox.height() 44 | }; 45 | match self.tool { 46 | Tool::Pixel => { 47 | sandbox.set_element(x, y, self.element); 48 | } 49 | Tool::Circle => { 50 | let radius_sq = (half_size * half_size) as isize; 51 | for cy in y1..y2 { 52 | for cx in x1..x2 { 53 | let dx = (cx as isize - x as isize).abs(); 54 | let dy = (cy as isize - y as isize).abs(); 55 | if dx * dx + dy * dy <= radius_sq { 56 | sandbox.set_element(cx, cy, self.element); 57 | } 58 | } 59 | } 60 | } 61 | Tool::Square => { 62 | for cy in y1..y2 { 63 | for cx in x1..x2 { 64 | sandbox.set_element(cx, cy, self.element); 65 | } 66 | } 67 | } 68 | Tool::Spray => { 69 | let radius_sq = (half_size * half_size) as isize; 70 | let count = if half_size > 3 { half_size / 3 } else { 1 }; 71 | for _ in 0..count { 72 | let cx = x1 + self.random.next() as usize % (x2 - x1); 73 | let cy = y1 + self.random.next() as usize % (y2 - y1); 74 | let dx = (cx as isize - x as isize).abs(); 75 | let dy = (cy as isize - y as isize).abs(); 76 | if dx * dx + dy * dy <= radius_sq { 77 | sandbox.set_element(cx, cy, self.element); 78 | } 79 | } 80 | } 81 | Tool::Fill => { 82 | let mut checklist = Vec::new(); 83 | let element_to_replace = sandbox.get(x, y).element; 84 | if element_to_replace == self.element 85 | || element_to_replace == Element::Indestructible 86 | { 87 | return; 88 | } 89 | sandbox.set_element(x, y, self.element); 90 | checklist.push((x, y)); 91 | while !checklist.is_empty() { 92 | let (x, y) = checklist.pop().unwrap(); 93 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 94 | let neighbor_element = sandbox.get(nx, ny).element; 95 | if neighbor_element == element_to_replace 96 | && neighbor_element != Element::Indestructible 97 | { 98 | sandbox.set_element(nx, ny, self.element); 99 | checklist.push((nx, ny)); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | impl Default for ToolBox { 109 | fn default() -> Self { 110 | Self { 111 | tool: Tool::Circle, 112 | element: Element::Sand, 113 | tool_size: 8, 114 | random: PseudoRandom::new(), 115 | } 116 | } 117 | } 118 | 119 | impl fmt::Display for Tool { 120 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 121 | write!(f, "{:?}", self) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Hide console on Windows. Remove to get console logging and backtraces. 2 | #![windows_subsystem = "windows"] 3 | 4 | use bevy::{prelude::*, window::WindowResolution}; 5 | 6 | use render::render_system; 7 | use sandbox::*; 8 | 9 | use crate::interface::InterfacePlugin; 10 | use crate::simulation::{Simulation, simulation_system}; 11 | 12 | mod pseudo_random; 13 | mod render; 14 | mod sandbox; 15 | mod simulation; 16 | mod interface; 17 | 18 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)] 19 | pub enum SystemOrderLabel { 20 | PointerInput, 21 | } 22 | 23 | fn main() { 24 | App::new() 25 | .add_plugins(( 26 | DefaultPlugins 27 | .set(WindowPlugin { 28 | primary_window: Some(Window { 29 | title: "Falling Rust".to_string(), 30 | resolution: WindowResolution::new(1024.0, 600.0), 31 | present_mode: bevy::window::PresentMode::Fifo, 32 | ..default() 33 | }), 34 | ..default() 35 | }) 36 | .set(ImagePlugin::default_nearest()), 37 | InterfacePlugin 38 | )) 39 | .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) 40 | .init_resource::() 41 | .add_systems(Startup, setup) 42 | .add_systems(Update, (simulation_system, render_system).chain()) 43 | .run(); 44 | } 45 | 46 | fn setup(mut commands: Commands, mut images: ResMut>) { 47 | commands.spawn(Camera2dBundle::default()); 48 | spawn_sandbox(commands, images.as_mut(), 256, 256); 49 | } 50 | -------------------------------------------------------------------------------- /src/pseudo_random.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct PseudoRandom { 3 | pub next: u32, 4 | } 5 | 6 | // Quick and dirty pseudo-random number generator. 7 | impl PseudoRandom { 8 | pub fn new() -> Self { 9 | Self { next: 12345 } 10 | } 11 | 12 | #[inline(always)] 13 | pub fn next(&mut self) -> u32 { 14 | self.next ^= self.next << 13; 15 | self.next ^= self.next >> 17; 16 | self.next ^= self.next << 5; 17 | self.next 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy::utils::Instant; 3 | 4 | use crate::sandbox::*; 5 | 6 | // "Render" the world by copying the element cells to pixels on a texture 7 | pub fn render_system( 8 | mut images: ResMut>, 9 | mut sandbox: Query<(&mut SandBox, &Handle)>, 10 | ) { 11 | let sandbox = sandbox.get_single_mut(); 12 | if sandbox.is_err() { 13 | // Sandbox not active, so skip this 14 | return; 15 | } 16 | let (mut sandbox, image_handle) = sandbox.unwrap(); 17 | 18 | let start = Instant::now(); 19 | 20 | let image = images.get_mut(image_handle).unwrap(); 21 | for y in 0..sandbox.height() { 22 | for x in 0..sandbox.width() { 23 | let color = element_type(sandbox.get_mut(x, y).element).color; 24 | let index = (x + y * sandbox.width()) * 4; 25 | image.data[index] = color.0; 26 | image.data[index + 1] = color.1; 27 | image.data[index + 2] = color.2; 28 | image.data[index + 3] = 255; 29 | } 30 | } 31 | 32 | let duration = Instant::now() - start; 33 | sandbox.render_time_ms = duration.as_millis(); 34 | } -------------------------------------------------------------------------------- /src/sandbox/cell.rs: -------------------------------------------------------------------------------- 1 | use crate::sandbox::*; 2 | 3 | // A cell that contains the state of a single pixel in the sand box. 4 | #[derive(Clone, Debug)] 5 | pub struct Cell { 6 | // Element in this cell 7 | pub element: Element, 8 | // Generic data fields, usage depends on element 9 | pub variant: u8, 10 | pub strength: u8, 11 | // Toggles each simulation step, to avoid duplicate simulation 12 | pub visited: bool, 13 | } 14 | 15 | impl Cell { 16 | // Reduce strength and turn into the given element of strength is zero 17 | pub fn dissolve_to(&mut self, element: Element) -> bool { 18 | if self.strength > 0 { 19 | self.strength -= 1; 20 | false 21 | } else { 22 | self.element = element; 23 | self.strength = element_type(element).strength; 24 | true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/sandbox/element.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub const ELEMENT_COUNT: usize = 27; 4 | 5 | // The different element types that live in a cell in the sand box 6 | #[repr(u8)] 7 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 8 | pub enum Element { 9 | Air = 0, 10 | Sand = 1, 11 | Rock = 2, 12 | Water = 3, 13 | Acid = 4, 14 | Drain = 5, 15 | Wood = 6, 16 | Iron = 7, 17 | Rust = 8, 18 | Fire = 9, 19 | Ash = 10, 20 | Oil = 11, 21 | Lava = 12, 22 | Smoke = 13, 23 | Life = 14, 24 | Seed = 15, 25 | Plant = 16, 26 | TNT = 17, 27 | Gunpowder = 18, 28 | Fuse = 19, 29 | Explosion = 20, 30 | WaterSource = 21, 31 | AcidSource = 22, 32 | OilSource = 23, 33 | FireSource = 24, 34 | LavaSource = 25, 35 | Indestructible = 26, 36 | } 37 | 38 | pub const FLAG_DISSOLVES_IN_ACID: u32 = 0b00000000000000000000000000000001; 39 | pub const FLAG_BURNS: u32 = 0b00000000000000000000000000000010; 40 | pub const FLAG_CAUSES_RUST: u32 = 0b00000000000000000000000000000100; 41 | pub const FLAG_TURNS_INTO_ASH: u32 = 0b00000000000000000000000000001000; 42 | pub const FLAG_NUTRITIOUS: u32 = 0b00000000000000000000000000010000; 43 | pub const FLAG_WET: u32 = 0b00000000000000000000000000100000; 44 | pub const FLAG_ALLOW_PLANT: u32 = 0b00000000000000000000000001000000; 45 | pub const FLAG_IS_SOURCE: u32 = 0b00000000000000000000000010000000; 46 | pub const FLAG_IGNITES: u32 = 0b00000000000000000000000100000000; 47 | pub const FLAG_BLAST_RESISTANT: u32 = 0b00000000000000000000001000000000; 48 | pub const FLAG_ACIDIC: u32 = 0b00000000000000000000010000000000; 49 | 50 | // Definition of an element type 51 | #[derive(Clone, Debug)] 52 | pub struct ElementType { 53 | pub form: ElementForm, 54 | pub strength: u8, 55 | pub weight: u8, 56 | pub color: (u8, u8, u8), 57 | pub flags: u32, 58 | pub source_element: Element, 59 | } 60 | 61 | impl ElementType { 62 | pub fn has_flag(&self, flag: u32) -> bool { 63 | self.flags & flag > 0 64 | } 65 | } 66 | 67 | #[inline(always)] 68 | pub fn element_type(element: Element) -> &'static ElementType { 69 | &ELEMENTS[element as usize] 70 | } 71 | 72 | // All element definitions. Note that the order must be identical to that in the Element enum. 73 | pub static ELEMENTS: [ElementType; ELEMENT_COUNT] = [ 74 | // Air = 0 75 | ElementType { 76 | form: ElementForm::Gas, 77 | strength: 1, 78 | weight: 128, 79 | color: (33, 122, 238), 80 | flags: FLAG_ALLOW_PLANT, 81 | source_element: Element::Air, 82 | }, // Sand = 1, 83 | ElementType { 84 | form: ElementForm::Powder, 85 | strength: 8, 86 | weight: 1, 87 | color: (224, 198, 98), 88 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_NUTRITIOUS | FLAG_ALLOW_PLANT, 89 | source_element: Element::Air, 90 | }, // Rock = 2, 91 | ElementType { 92 | form: ElementForm::Solid, 93 | strength: 1, 94 | weight: 1, 95 | color: (107, 104, 104), 96 | flags: FLAG_BLAST_RESISTANT, 97 | source_element: Element::Air, 98 | }, // Water = 3, 99 | ElementType { 100 | form: ElementForm::Liquid, 101 | strength: 12, 102 | weight: 128, 103 | color: (16, 16, 128), 104 | flags: FLAG_CAUSES_RUST | FLAG_WET | FLAG_ALLOW_PLANT, 105 | source_element: Element::Air, 106 | }, // Acid = 4, 107 | ElementType { 108 | form: ElementForm::Liquid, 109 | strength: 10, 110 | weight: 32, 111 | color: (182, 255, 5), 112 | flags: FLAG_ACIDIC, 113 | source_element: Element::Air, 114 | }, // Drain = 5, 115 | ElementType { 116 | form: ElementForm::Solid, 117 | strength: 1, 118 | weight: 1, 119 | color: (0, 0, 0), 120 | flags: 0, 121 | source_element: Element::Air, 122 | }, // Wood = 6, 123 | ElementType { 124 | form: ElementForm::Solid, 125 | strength: 16, 126 | weight: 1, 127 | color: (122, 57, 0), 128 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_BURNS | FLAG_TURNS_INTO_ASH | FLAG_BLAST_RESISTANT, 129 | source_element: Element::Air, 130 | }, // Iron = 7, 131 | ElementType { 132 | form: ElementForm::Solid, 133 | strength: 64, 134 | weight: 1, 135 | color: (160, 157, 157), 136 | flags: FLAG_BLAST_RESISTANT, 137 | source_element: Element::Air, 138 | }, // Rust = 8, 139 | ElementType { 140 | form: ElementForm::Powder, 141 | strength: 1, 142 | weight: 1, 143 | color: (115, 50, 2), 144 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_CAUSES_RUST, 145 | source_element: Element::Air, 146 | }, // Fire = 9, 147 | ElementType { 148 | form: ElementForm::Gas, 149 | strength: 64, 150 | weight: 64, 151 | color: (255, 225, 136), 152 | flags: FLAG_IGNITES, 153 | source_element: Element::Air, 154 | }, // Ash = 10, 155 | ElementType { 156 | form: ElementForm::Powder, 157 | strength: 16, 158 | weight: 1, 159 | color: (214, 220, 234), 160 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_NUTRITIOUS | FLAG_ALLOW_PLANT, 161 | source_element: Element::Air, 162 | }, // Oil = 11, 163 | ElementType { 164 | form: ElementForm::Liquid, 165 | strength: 10, 166 | weight: 64, 167 | color: (64, 32, 64), 168 | flags: FLAG_BURNS, 169 | source_element: Element::Air, 170 | }, // Lava = 12, 171 | ElementType { 172 | form: ElementForm::Liquid, 173 | strength: 4, 174 | weight: 192, 175 | color: (180, 64, 16), 176 | flags: FLAG_IGNITES, 177 | source_element: Element::Air, 178 | }, // Smoke = 13, 179 | ElementType { 180 | form: ElementForm::Gas, 181 | strength: 32, 182 | weight: 32, 183 | color: (8, 8, 8), 184 | flags: 0, 185 | source_element: Element::Air, 186 | }, // Life = 14, 187 | ElementType { 188 | form: ElementForm::Solid, 189 | strength: 2, 190 | weight: 1, 191 | color: (210, 255, 210), 192 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_BURNS | FLAG_TURNS_INTO_ASH, 193 | source_element: Element::Air, 194 | }, // Seed = 15, 195 | ElementType { 196 | form: ElementForm::Powder, 197 | strength: 32, 198 | weight: 1, 199 | color: (170, 220, 130), 200 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_NUTRITIOUS, 201 | source_element: Element::Air, 202 | }, // Plant = 16, 203 | ElementType { 204 | form: ElementForm::Solid, 205 | strength: 1, 206 | weight: 1, 207 | color: (60, 200, 30), 208 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_BURNS | FLAG_NUTRITIOUS, 209 | source_element: Element::Air, 210 | }, // TNT = 17, 211 | ElementType { 212 | form: ElementForm::Solid, 213 | strength: 3, 214 | weight: 1, 215 | color: (200, 32, 16), 216 | flags: FLAG_DISSOLVES_IN_ACID, 217 | source_element: Element::Air, 218 | }, // Gunpowder = 18, 219 | ElementType { 220 | form: ElementForm::Powder, 221 | strength: 2, 222 | weight: 2, 223 | color: (122, 21, 3), 224 | flags: FLAG_DISSOLVES_IN_ACID, 225 | source_element: Element::Air, 226 | }, // Fuse = 19, 227 | ElementType { 228 | form: ElementForm::Solid, 229 | strength: 1, 230 | weight: 1, 231 | color: (211, 80, 91), 232 | flags: FLAG_DISSOLVES_IN_ACID | FLAG_BURNS, 233 | source_element: Element::Air, 234 | }, // Explosion = 20, 235 | ElementType { 236 | form: ElementForm::Solid, 237 | strength: 1, 238 | weight: 1, 239 | color: (245, 220, 200), 240 | flags: 0, 241 | source_element: Element::Air, 242 | }, // WaterSource = 21, 243 | ElementType { 244 | form: ElementForm::Solid, 245 | strength: 1, 246 | weight: 1, 247 | color: (16, 16, 255), 248 | flags: FLAG_IS_SOURCE, 249 | source_element: Element::Water, 250 | }, // AcidSource = 22 251 | ElementType { 252 | form: ElementForm::Solid, 253 | strength: 1, 254 | weight: 1, 255 | color: (160, 255, 64), 256 | flags: FLAG_IS_SOURCE, 257 | source_element: Element::Acid, 258 | }, // OilSource = 23, 259 | ElementType { 260 | form: ElementForm::Solid, 261 | strength: 1, 262 | weight: 1, 263 | color: (32, 8, 32), 264 | flags: FLAG_IS_SOURCE, 265 | source_element: Element::Oil, 266 | }, // FireSource = 24, 267 | ElementType { 268 | form: ElementForm::Solid, 269 | strength: 1, 270 | weight: 1, 271 | color: (255, 255, 163), 272 | flags: FLAG_IS_SOURCE | FLAG_IGNITES, 273 | source_element: Element::Fire, 274 | }, // LavaSource = 25, 275 | ElementType { 276 | form: ElementForm::Solid, 277 | strength: 1, 278 | weight: 1, 279 | color: (255, 128, 32), 280 | flags: FLAG_IS_SOURCE, 281 | source_element: Element::Lava, 282 | }, // Indestructible = 26, 283 | ElementType { 284 | form: ElementForm::Solid, 285 | strength: 1, 286 | weight: 1, 287 | color: (64, 40, 40), 288 | flags: 0, 289 | source_element: Element::Air, 290 | }, 291 | ]; 292 | 293 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 294 | pub enum ElementForm { 295 | Solid, 296 | Powder, 297 | Liquid, 298 | Gas, 299 | } 300 | 301 | impl fmt::Display for Element { 302 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 303 | write!(f, "{:?}", self) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/sandbox/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::*, 3 | render::render_resource::{Extent3d, TextureDimension, TextureFormat}, 4 | }; 5 | 6 | mod cell; 7 | mod element; 8 | 9 | pub use cell::*; 10 | pub use element::*; 11 | 12 | // The sandbox consisting of a grid of cells with elements that is simulated 13 | #[derive(Component)] 14 | pub struct SandBox { 15 | width: usize, 16 | height: usize, 17 | cells: Vec, 18 | visited_state: bool, 19 | pub render_time_ms: u128, 20 | } 21 | 22 | impl SandBox { 23 | pub fn new(width: usize, height: usize) -> Self { 24 | let mut sandbox = SandBox::empty(width, height); 25 | // Set indestructible pixels at the border to ease computations 26 | for x in 0..sandbox.width() { 27 | sandbox.set_element(x, 0, Element::Indestructible); 28 | sandbox.set_element(x, sandbox.height() - 1, Element::Indestructible); 29 | } 30 | for y in 0..sandbox.height() { 31 | sandbox.set_element(0, y, Element::Indestructible); 32 | sandbox.set_element(sandbox.width() - 1, y, Element::Indestructible); 33 | } 34 | sandbox 35 | } 36 | 37 | fn empty(width: usize, height: usize) -> Self { 38 | SandBox { 39 | width, 40 | height, 41 | cells: vec![ 42 | Cell { 43 | element: Element::Air, 44 | variant: 0, 45 | strength: 0, 46 | visited: false, 47 | }; 48 | width * height 49 | ], 50 | visited_state: false, 51 | render_time_ms: 0, 52 | } 53 | } 54 | 55 | pub fn get(&self, x: usize, y: usize) -> &Cell { 56 | let index = self.index(x, y); 57 | &self.cells[index] 58 | } 59 | 60 | pub fn get_mut(&mut self, x: usize, y: usize) -> &mut Cell { 61 | let index = self.index(x, y); 62 | &mut self.cells[index] 63 | } 64 | 65 | pub fn reduce_strength(&mut self, x: usize, y: usize, amount: u8) -> bool { 66 | let index = self.index(x, y); 67 | let cell = &mut self.cells[index]; 68 | if cell.strength > 0 { 69 | cell.strength = if cell.strength > amount { 70 | cell.strength - amount 71 | } else { 72 | 0 73 | }; 74 | true 75 | } else { 76 | false 77 | } 78 | } 79 | 80 | pub fn clear_cell(&mut self, x: usize, y: usize) { 81 | self.set_element(x, y, Element::Air); 82 | } 83 | 84 | pub fn set_element_with_strength( 85 | &mut self, 86 | x: usize, 87 | y: usize, 88 | element: Element, 89 | strength: u8, 90 | ) { 91 | let index = self.index(x, y); 92 | let cell = &mut self.cells[index]; 93 | if cell.element == Element::Indestructible { 94 | // Cannot edit these blocks 95 | return; 96 | } 97 | cell.element = element; 98 | cell.visited = self.visited_state; 99 | cell.strength = strength; 100 | } 101 | 102 | pub fn set_element(&mut self, x: usize, y: usize, element: Element) { 103 | self.set_element_with_strength(x, y, element, element_type(element).strength); 104 | } 105 | 106 | pub fn swap(&mut self, x: usize, y: usize, x2: usize, y2: usize) { 107 | let index1 = self.index(x, y); 108 | let index2 = self.index(x2, y2); 109 | let mut cell = self.cells[index1].clone(); 110 | let mut cell2 = self.cells[index2].clone(); 111 | if cell.element == Element::Indestructible || cell2.element == Element::Indestructible { 112 | // Cannot edit these blocks 113 | return; 114 | } 115 | // cell is moved to the place of cell 2, so becomes the second cell 116 | cell.visited = self.visited_state; 117 | cell2.visited = self.visited_state; 118 | self.cells[index1] = cell2; 119 | self.cells[index2] = cell; 120 | } 121 | 122 | pub fn set_visited(&mut self, x: usize, y: usize) { 123 | let index = self.index(x, y); 124 | self.cells[index].visited = self.visited_state; 125 | } 126 | 127 | pub fn width(&self) -> usize { 128 | self.width 129 | } 130 | 131 | pub fn height(&self) -> usize { 132 | self.height 133 | } 134 | 135 | pub fn toggle_visited_state(&mut self) -> bool { 136 | self.visited_state = !self.visited_state; 137 | self.visited_state 138 | } 139 | 140 | pub fn is_visited_state(&self) -> bool { 141 | self.visited_state 142 | } 143 | 144 | #[inline(always)] 145 | fn index(&self, x: usize, y: usize) -> usize { 146 | x + y * self.width 147 | } 148 | } 149 | 150 | pub fn spawn_sandbox(mut commands: Commands, images: &mut Assets, width: u32, height: u32) { 151 | let image_handle = { 152 | let image = Image::new_fill( 153 | Extent3d { 154 | width, 155 | height, 156 | depth_or_array_layers: 1, 157 | }, 158 | TextureDimension::D2, 159 | &[255, 0, 0, 255], 160 | TextureFormat::Rgba8UnormSrgb, 161 | ); 162 | images.add(image) 163 | }; 164 | commands 165 | .spawn(SandBox::new(width as usize, height as usize)) 166 | .insert(SpriteBundle { 167 | texture: image_handle, 168 | transform: Transform { 169 | translation: Vec3::new(0.0, 0.0, 0.0), 170 | ..Default::default() 171 | }, 172 | ..Default::default() 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /src/simulation.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy::utils::Instant; 3 | 4 | use crate::pseudo_random::PseudoRandom; 5 | use crate::sandbox::*; 6 | 7 | #[derive(Clone, Resource)] 8 | pub struct Simulation { 9 | pub running: bool, 10 | pub step: bool, 11 | pub frame_time_ms: u128, 12 | pub random: PseudoRandom, 13 | } 14 | 15 | impl Default for Simulation { 16 | fn default() -> Self { 17 | Simulation::new() 18 | } 19 | } 20 | 21 | impl Simulation { 22 | pub fn new() -> Self { 23 | Self { 24 | running: true, 25 | step: false, 26 | frame_time_ms: 0, 27 | random: PseudoRandom::new(), 28 | } 29 | } 30 | } 31 | 32 | // System used to simulate the world a single step each frame 33 | pub fn simulation_system(mut sandbox: Query<&mut SandBox>, mut simulation: ResMut) { 34 | match sandbox.get_single_mut() { 35 | Ok(mut sandbox) => { 36 | simulation_step(simulation.as_mut(), sandbox.as_mut()); 37 | } 38 | Err(_) => { 39 | return; 40 | } 41 | } 42 | } 43 | 44 | pub fn simulation_step(simulation: &mut Simulation, sandbox: &mut SandBox) { 45 | let start = Instant::now(); 46 | if simulation.running || simulation.step { 47 | simulation.step = false; 48 | let visited = sandbox.toggle_visited_state(); 49 | let (width, height) = (sandbox.width() - 1, sandbox.height() - 1); 50 | for y in (1..height).rev() { 51 | // Switch X order every frame to avoid simulation artifacts 52 | if visited { 53 | for x in 1..width { 54 | update_cell(x, y, sandbox, simulation.random.next()); 55 | } 56 | } else { 57 | for x in (1..width).rev() { 58 | update_cell(x, y, sandbox, simulation.random.next()); 59 | } 60 | } 61 | } 62 | } 63 | let duration = Instant::now() - start; 64 | simulation.frame_time_ms = duration.as_millis(); 65 | } 66 | 67 | fn update_cell(x: usize, y: usize, sandbox: &mut SandBox, random: u32) { 68 | // Step 1: handle interactions with surrounding cells 69 | let cell = sandbox.get(x, y).clone(); 70 | if cell.visited == sandbox.is_visited_state() { 71 | // Visited this one already 72 | return; 73 | } 74 | let cell_type = element_type(cell.element); 75 | 76 | // Generic element effects 77 | if cell_type.has_flag(FLAG_IGNITES) { 78 | handle_igniting_cell(x, y, sandbox, random); 79 | } 80 | 81 | if cell_type.has_flag(FLAG_ACIDIC) { 82 | handle_acidic_cell(x, y, sandbox, random); 83 | } 84 | 85 | if cell_type.has_flag(FLAG_IS_SOURCE) { 86 | handle_source_cell(x, y, sandbox, cell_type); 87 | } 88 | 89 | // Element-specific handling 90 | let mut marked_as_visited = match cell.element { 91 | Element::Air => update_air(x, y, sandbox), 92 | Element::Water => update_water(x, y, sandbox, random), 93 | Element::Drain => update_drain(x, y, sandbox, random), 94 | Element::Fire => update_fire(x, y, sandbox, random), 95 | Element::Ash => update_ash(x, y, sandbox, random), 96 | Element::Lava => update_lava(x, y, sandbox, random), 97 | Element::Smoke => update_smoke(x, y, sandbox, random), 98 | Element::Life => update_life(x, y, sandbox), 99 | Element::Iron => update_iron(x, y, sandbox, random), 100 | Element::Plant => update_plant(x, y, sandbox, random), 101 | Element::Seed => update_seed(x, y, sandbox), 102 | Element::TNT => update_explosive(Element::TNT, x, y, sandbox), 103 | Element::Gunpowder => update_explosive(Element::Gunpowder, x, y, sandbox), 104 | Element::Explosion => update_explosion(x, y, sandbox, random), 105 | _ => false, 106 | }; 107 | 108 | // Element form handling (movement) 109 | match cell_type.form { 110 | ElementForm::Solid => {} 111 | ElementForm::Powder => { 112 | marked_as_visited = handle_powder_form(sandbox, x, y, random); 113 | } 114 | ElementForm::Liquid => { 115 | marked_as_visited = handle_liquid_form(sandbox, x, y, random); 116 | } 117 | ElementForm::Gas => { 118 | marked_as_visited = handle_gas_form(sandbox, x, y, random); 119 | } 120 | } 121 | 122 | if !marked_as_visited { 123 | sandbox.set_visited(x, y); 124 | } 125 | } 126 | 127 | fn handle_igniting_cell(x: usize, y: usize, sandbox: &mut SandBox, random: u32) { 128 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 129 | let neighbor_cell = sandbox.get(nx, ny); 130 | let neighbor_type = element_type(neighbor_cell.element); 131 | if neighbor_cell.element == Element::TNT || neighbor_cell.element == Element::Gunpowder { 132 | sandbox.set_element_with_strength( 133 | nx, 134 | ny, 135 | Element::Explosion, 136 | neighbor_cell.strength, 137 | ); 138 | } else if neighbor_type.has_flag(FLAG_BURNS) { 139 | if neighbor_type.has_flag(FLAG_TURNS_INTO_ASH) && once_per(random, 3) { 140 | sandbox.get_mut(nx, ny).dissolve_to(Element::Ash); 141 | } else { 142 | sandbox.get_mut(nx, ny).dissolve_to(Element::Fire); 143 | } 144 | } 145 | } 146 | } 147 | 148 | fn handle_acidic_cell(x: usize, y: usize, sandbox: &mut SandBox, random: u32) { 149 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 150 | let neighbor_cell = sandbox.get_mut(nx, ny); 151 | let neighbor_type = element_type(neighbor_cell.element); 152 | if neighbor_type.has_flag(FLAG_DISSOLVES_IN_ACID) 153 | && once_per(random, (neighbor_cell.strength / 2).max(2) as u32) 154 | { 155 | if sandbox.get_mut(nx, ny).dissolve_to(Element::Air) { 156 | if once_per(random, 2) { 157 | sandbox.set_element(x, y, Element::Smoke); 158 | } else { 159 | sandbox.clear_cell(x, y); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | fn handle_powder_form(sandbox: &mut SandBox, x: usize, y: usize, random: u32) -> bool { 167 | // Can we fall down? 168 | let below_element = sandbox.get(x, y + 1).element; 169 | let below_element_type = element_type(below_element); 170 | if below_element_type.form == ElementForm::Liquid || below_element_type.form == ElementForm::Gas 171 | { 172 | sandbox.swap(x, y, x, y + 1); 173 | return true; 174 | } 175 | // Can we slide off diagonally? 176 | let neighbor_x = random_neighbor_x(x, random); 177 | let neighbor_element = sandbox.get(neighbor_x, y + 1).element; 178 | let neighbor_type = element_type(neighbor_element); 179 | if neighbor_type.form == ElementForm::Liquid || neighbor_type.form == ElementForm::Gas { 180 | sandbox.swap(x, y, neighbor_x, y + 1); 181 | return true; 182 | } 183 | // Can we slide of diagonally the other way? 184 | let neighbor_x = random_other_neighbor_x(x, random); 185 | let neighbor_element = sandbox.get(neighbor_x, y + 1).element; 186 | let neighbor_type = element_type(neighbor_element); 187 | if neighbor_type.form == ElementForm::Liquid || neighbor_type.form == ElementForm::Gas { 188 | sandbox.swap(x, y, neighbor_x, y + 1); 189 | return true; 190 | } 191 | false 192 | } 193 | 194 | fn handle_liquid_form(sandbox: &mut SandBox, x: usize, y: usize, random: u32) -> bool { 195 | let cell = sandbox.get(x, y).clone(); 196 | let cell_element_type = element_type(cell.element); 197 | 198 | let random_60 = random % 60; 199 | let check_x = if random_60 < 58 { 200 | x 201 | } else if random_60 == 58 { 202 | x - 1 203 | } else { 204 | x + 1 205 | }; 206 | 207 | // Liquid falls down in gas or when heavier than the element below 208 | let below_element = sandbox.get(check_x, y + 1).element; 209 | let below_element_type = element_type(below_element); 210 | if below_element_type.form == ElementForm::Gas 211 | || (below_element_type.form == ElementForm::Liquid 212 | && below_element != cell.element 213 | && below_element_type.weight < cell_element_type.weight 214 | && once_per(random, 3)) 215 | { 216 | sandbox.swap(x, y, check_x, y + 1); 217 | return true; 218 | } 219 | 220 | // Liquid flows sideways. Strength of the cell indicates the speed of sideways flow. 221 | let check_left = once_per(random, 2); 222 | for n in 1..cell.strength as usize { 223 | let check_x_opt = if check_left { 224 | if x > n { 225 | Some(x - n) 226 | } else { 227 | None 228 | } 229 | } else { 230 | if x + n < sandbox.width() - 1 { 231 | Some(x + n) 232 | } else { 233 | None 234 | } 235 | }; 236 | if let Some(check_x) = check_x_opt { 237 | let neighbor = sandbox.get(check_x, y); 238 | let neighbor_element_type = element_type(neighbor.element); 239 | if neighbor_element_type.form == ElementForm::Gas 240 | || (neighbor_element_type.form == ElementForm::Liquid 241 | && neighbor.element != cell.element 242 | && neighbor_element_type.weight < cell_element_type.weight 243 | && once_per(random, 3)) 244 | { 245 | // Slide sideways 246 | sandbox.swap(x, y, check_x, y); 247 | return true; 248 | } 249 | if neighbor.element != cell.element { 250 | break; 251 | } 252 | } else { 253 | break; 254 | } 255 | } 256 | 257 | true 258 | } 259 | 260 | fn handle_gas_form(sandbox: &mut SandBox, x: usize, y: usize, random: u32) -> bool { 261 | let cell = sandbox.get(x, y).clone(); 262 | let cell_element_type = element_type(cell.element); 263 | 264 | // Move in a random direction, with a tendency upwards 265 | let (nx, ny) = match random % 5 { 266 | 0 => (x + 1, y), 267 | 1 => (x - 1, y), 268 | _ => (x, y - 1), 269 | }; 270 | let neighbor_element = sandbox.get(nx, ny).element; 271 | let neighbor_element_type = element_type(neighbor_element); 272 | if neighbor_element_type.form == ElementForm::Gas 273 | && cell.element != neighbor_element 274 | && neighbor_element_type.weight > cell_element_type.weight 275 | && (cell.element == Element::Air || once_per(random, 2)) 276 | { 277 | sandbox.swap(x, y, nx, ny); 278 | return true; 279 | } 280 | false 281 | } 282 | 283 | fn handle_source_cell( 284 | x: usize, 285 | y: usize, 286 | sandbox: &mut SandBox, 287 | cell_type: &ElementType, 288 | ) { 289 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 290 | if sandbox.get(nx, ny).element == Element::Air { 291 | sandbox.set_element(nx, ny, cell_type.source_element); 292 | } 293 | } 294 | } 295 | 296 | fn update_water(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 297 | let (nx, ny) = match random % 4 { 298 | 0 => (x - 1, y), 299 | 1 => (x + 1, y), 300 | 2 => (x, y - 1), 301 | _ => (x, y + 1), 302 | }; 303 | let neighbor_element = sandbox.get(nx, ny).element; 304 | match neighbor_element { 305 | Element::Acid => { 306 | sandbox.get_mut(nx, ny).dissolve_to(Element::Water); 307 | return false; 308 | } 309 | Element::Lava => { 310 | if sandbox.get_mut(nx, ny).dissolve_to(Element::Rock) { 311 | sandbox.clear_cell(x, y); 312 | } 313 | return false; 314 | } 315 | Element::Fire => { 316 | sandbox.clear_cell(x, y); 317 | sandbox.set_element(nx, ny, Element::Water); 318 | return true; 319 | } 320 | _ => {} 321 | } 322 | false 323 | } 324 | 325 | fn update_drain(x: usize, y: usize, sandbox: &mut SandBox, _random: u32) -> bool { 326 | // Remove any liquid on top, left or right of this cell 327 | let element_form = element_type(sandbox.get(x, y - 1).element).form; 328 | if element_form == ElementForm::Liquid { 329 | sandbox.clear_cell(x, y - 1); 330 | return true; 331 | } 332 | let element_form = element_type(sandbox.get(x - 1, y).element).form; 333 | if element_form == ElementForm::Liquid { 334 | sandbox.clear_cell(x - 1, y); 335 | return true; 336 | } 337 | let element_form = element_type(sandbox.get(x + 1, y).element).form; 338 | if element_form == ElementForm::Liquid { 339 | sandbox.clear_cell(x + 1, y); 340 | return true; 341 | } 342 | false 343 | } 344 | 345 | fn update_fire(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 346 | // Reduce fire strength over time 347 | if once_per(random, 2) && sandbox.get_mut(x, y).dissolve_to(Element::Air) { 348 | sandbox.set_element(x, y, Element::Smoke); 349 | return true; 350 | } 351 | false 352 | } 353 | 354 | fn update_ash(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 355 | if once_per(random, 100) && sandbox.get_mut(x, y).dissolve_to(Element::Air) { 356 | return true; 357 | } 358 | false 359 | } 360 | 361 | fn update_lava(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 362 | let cell = sandbox.get_mut(x, y); 363 | // Cool down when no longer at max hotness 364 | if once_per(random, 2) && cell.strength < element_type(Element::Lava).strength { 365 | if sandbox.get_mut(x, y).dissolve_to(Element::Rock) { 366 | return true; 367 | } 368 | } 369 | // Give off sparks 370 | if once_per(random, 100) && sandbox.get(x, y - 1).element == Element::Air { 371 | sandbox.set_element(x, y - 1, Element::Fire); 372 | } 373 | false 374 | } 375 | 376 | fn update_smoke(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 377 | if once_per(random, 2) && sandbox.get_mut(x, y).dissolve_to(Element::Air) { 378 | sandbox.clear_cell(x, y); 379 | return true; 380 | } 381 | false 382 | } 383 | 384 | fn update_iron(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 385 | let mut rusty_neighbor = false; 386 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 387 | let element = sandbox.get(nx, ny).element; 388 | if element_type(element).has_flag(FLAG_CAUSES_RUST) { 389 | rusty_neighbor = true; 390 | break; 391 | } 392 | } 393 | if rusty_neighbor { 394 | // Rust iron by reducing its strength somewhat randomly 395 | if once_per(random, 3) && !sandbox.reduce_strength(x, y, 1) { 396 | // Turn into rust 397 | sandbox.set_element(x, y, Element::Rust); 398 | return true; 399 | } 400 | } 401 | false 402 | } 403 | 404 | fn update_seed(x: usize, y: usize, sandbox: &mut SandBox) -> bool { 405 | // Check if we have water and nutrition 406 | let mut nutrition = false; 407 | let mut water = false; 408 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 409 | let neighbor_element = sandbox.get(nx, ny).element; 410 | if !nutrition && element_type(neighbor_element).has_flag(FLAG_NUTRITIOUS) { 411 | nutrition = true; 412 | } 413 | if !water && element_type(neighbor_element).has_flag(FLAG_WET) { 414 | water = true; 415 | } 416 | } 417 | 418 | if nutrition && water { 419 | // Convert to a new plant 420 | sandbox.set_element_with_strength( 421 | x, 422 | y, 423 | Element::Plant, 424 | element_type(Element::Seed).strength, 425 | ); 426 | sandbox.get_mut(x, y).variant = element_type(Element::Seed).strength; 427 | true 428 | } else { 429 | false 430 | } 431 | } 432 | 433 | fn update_plant(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 434 | let (cell_strength, cell_variant) = { 435 | let cell = sandbox.get(x, y); 436 | (cell.strength, cell.variant) 437 | }; 438 | if cell_variant <= 1 { 439 | // Sometimes turns into seed 440 | if once_per(random, 5) { 441 | sandbox.set_element(x, y, Element::Seed); 442 | } 443 | } 444 | 445 | // Are we still attached to the plant? 446 | let mut attached = false; 447 | if cell_variant == element_type(Element::Seed).strength { 448 | // Root cell 449 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 450 | let neighbor = sandbox.get(nx, ny); 451 | if neighbor.element != Element::Plant 452 | && element_type(neighbor.element).has_flag(FLAG_NUTRITIOUS) 453 | { 454 | attached = true; 455 | break; 456 | } 457 | } 458 | } else { 459 | for (nx, ny) in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] { 460 | let neighbor = sandbox.get(nx, ny); 461 | if neighbor.element == Element::Plant && neighbor.variant > cell_variant { 462 | attached = true; 463 | break; 464 | } 465 | } 466 | } 467 | if !attached { 468 | // Not attached, so die 469 | sandbox.set_element(x, y, Element::Ash); 470 | return true; 471 | } 472 | if cell_strength <= 1 { 473 | // Not growing anymore 474 | return false; 475 | } 476 | // Plant is still growing 477 | let (nx, ny) = match random % 1000 { 478 | 0 | 1 => (x - 1, y), 479 | 2 | 3 => (x + 1, y), 480 | 4..=100 => (x, y - 1), 481 | _ => return false, 482 | }; 483 | let other_element = sandbox.get(nx, ny).element; 484 | let new_cell_strength = cell_strength - 1; 485 | if element_type(other_element).has_flag(FLAG_ALLOW_PLANT) { 486 | sandbox.set_element_with_strength(nx, ny, Element::Plant, new_cell_strength); 487 | sandbox.get_mut(nx, ny).variant = cell_variant - 1; 488 | sandbox.reduce_strength(x, y, new_cell_strength); 489 | } 490 | 491 | false 492 | } 493 | 494 | fn update_explosive(element: Element, x: usize, y: usize, sandbox: &mut SandBox) -> bool { 495 | let strength = sandbox.get(x, y).strength; 496 | if strength == element_type(element).strength { 497 | return false; 498 | } 499 | sandbox.set_element_with_strength(x, y, Element::Explosion, strength); 500 | true 501 | } 502 | 503 | fn update_explosion(x: usize, y: usize, sandbox: &mut SandBox, random: u32) -> bool { 504 | if !sandbox.reduce_strength(x, y, 1) { 505 | sandbox.set_element(x, y, Element::Fire); 506 | return true; 507 | } 508 | // Spread explosion 509 | let strength = sandbox.get(x, y).strength; 510 | let neighbors = match random % 2 { 511 | 0 => [(x - 1, y), (x + 1, y)], 512 | _ => [(x, y - 1), (x, y + 1)], 513 | }; 514 | for (nx, ny) in neighbors { 515 | let neighbor = sandbox.get_mut(nx, ny); 516 | if neighbor.element == Element::TNT || neighbor.element == Element::Gunpowder { 517 | let explosion_strength = if neighbor.strength + strength < 255 { 518 | neighbor.strength + strength 519 | } else { 520 | neighbor.strength 521 | }; 522 | sandbox.set_element_with_strength( 523 | nx, 524 | ny, 525 | Element::Explosion, 526 | explosion_strength, 527 | ); 528 | } else if neighbor.element != Element::Explosion { 529 | let neighbor_type = element_type(neighbor.element); 530 | let neighbor_strength = if neighbor_type.has_flag(FLAG_BLAST_RESISTANT) { 531 | neighbor.strength 532 | } else { 533 | 0 534 | }; 535 | if neighbor_strength < strength { 536 | sandbox.set_element_with_strength( 537 | nx, 538 | ny, 539 | Element::Explosion, 540 | strength - neighbor_strength, 541 | ); 542 | } 543 | } 544 | } 545 | true 546 | } 547 | 548 | fn update_air(x: usize, y: usize, sandbox: &mut SandBox) -> bool { 549 | let mut living_neighbors = 0; 550 | if sandbox.get(x - 1, y - 1).element == Element::Life { 551 | living_neighbors += 1; 552 | } 553 | if sandbox.get(x, y - 1).element == Element::Life { 554 | living_neighbors += 1; 555 | } 556 | if sandbox.get(x + 1, y - 1).element == Element::Life { 557 | living_neighbors += 1; 558 | } 559 | if sandbox.get(x - 1, y).element == Element::Life { 560 | living_neighbors += 1; 561 | } 562 | if sandbox.get(x + 1, y).element == Element::Life { 563 | living_neighbors += 1; 564 | } 565 | if sandbox.get(x - 1, y + 1).element == Element::Life { 566 | living_neighbors += 1; 567 | } 568 | if sandbox.get(x, y + 1).element == Element::Life { 569 | living_neighbors += 1; 570 | } 571 | if sandbox.get(x + 1, y + 1).element == Element::Life { 572 | living_neighbors += 1; 573 | } 574 | if living_neighbors == 3 { 575 | sandbox.set_element(x, y, Element::Life); 576 | return true; 577 | } 578 | false 579 | } 580 | 581 | fn update_life(x: usize, y: usize, sandbox: &mut SandBox) -> bool { 582 | let mut living_neighbors = 0; 583 | if sandbox.get(x - 1, y - 1).element == Element::Life { 584 | living_neighbors += 1; 585 | } 586 | if sandbox.get(x, y - 1).element == Element::Life { 587 | living_neighbors += 1; 588 | } 589 | if sandbox.get(x + 1, y - 1).element == Element::Life { 590 | living_neighbors += 1; 591 | } 592 | if sandbox.get(x - 1, y).element == Element::Life { 593 | living_neighbors += 1; 594 | } 595 | if sandbox.get(x + 1, y).element == Element::Life { 596 | living_neighbors += 1; 597 | } 598 | if sandbox.get(x - 1, y + 1).element == Element::Life { 599 | living_neighbors += 1; 600 | } 601 | if sandbox.get(x, y + 1).element == Element::Life { 602 | living_neighbors += 1; 603 | } 604 | if sandbox.get(x + 1, y + 1).element == Element::Life { 605 | living_neighbors += 1; 606 | } 607 | if living_neighbors < 2 || living_neighbors > 3 { 608 | sandbox.set_element(x, y, Element::Air); 609 | return true; 610 | } 611 | // Keep on living 612 | false 613 | } 614 | 615 | pub fn random_neighbor_x(x: usize, random: u32) -> usize { 616 | if random % 2 == 0 { 617 | x + 1 618 | } else { 619 | x - 1 620 | } 621 | } 622 | 623 | pub fn random_other_neighbor_x(x: usize, random: u32) -> usize { 624 | if random % 2 == 0 { 625 | x - 1 626 | } else { 627 | x + 1 628 | } 629 | } 630 | 631 | fn once_per(random: u32, count: u32) -> bool { 632 | random % count == 0 633 | } 634 | --------------------------------------------------------------------------------