diff --git a/assets/shaders/graphics.vert b/assets/shaders/graphics.vert index bbc59a2..aecbd7f 100644 --- a/assets/shaders/graphics.vert +++ b/assets/shaders/graphics.vert @@ -71,17 +71,29 @@ vec3 compute_light(ivec2 world_pos) { } void main() { - vec2 pixel = (in_grid + in_pos) * pc.cell_size; - gl_Position = vec4( - 2.0 * pixel.x / pc.screen_size.x - 1.0, - 2.0 * pixel.y / pc.screen_size.y - 1.0, - 0.0, 1.0 - ); - out_color = in_color; - if (pc.is_ui != 0u) { + if (pc.is_ui == 2u) { + vec2 world_pos = in_grid; + vec2 screen = (world_pos - vec2(pc.cam_pos)) * pc.cell_size + in_pos * pc.cell_size; + gl_Position = vec4( + 2.0 * screen.x / pc.screen_size.x - 1.0, + 2.0 * screen.y / pc.screen_size.y - 1.0, + 0.0, 1.0 + ); + out_color = in_color; out_light = vec3(1.0); } else { - ivec2 world_pos = ivec2(in_grid + vec2(pc.cam_pos)); - out_light = compute_light(world_pos); + vec2 pixel = (in_grid + in_pos) * pc.cell_size; + gl_Position = vec4( + 2.0 * pixel.x / pc.screen_size.x - 1.0, + 2.0 * pixel.y / pc.screen_size.y - 1.0, + 0.0, 1.0 + ); + out_color = in_color; + if (pc.is_ui != 0u) { + out_light = vec3(1.0); + } else { + ivec2 world_pos = ivec2(in_grid + vec2(pc.cam_pos)); + out_light = compute_light(world_pos); + } } } diff --git a/assets/shaders/graphics_vert.spv b/assets/shaders/graphics_vert.spv index 5da6ebb..1c82ede 100644 Binary files a/assets/shaders/graphics_vert.spv and b/assets/shaders/graphics_vert.spv differ diff --git a/benchmark_results.json b/benchmark_results.json index cf9f095..afeebad 100644 --- a/benchmark_results.json +++ b/benchmark_results.json @@ -1,17 +1,17 @@ { "mode": "graphics", "ticks": 300, - "total_time_ms": 1847.9, - "avg_fps": 162.3, - "avg_frame_time_ms": 6.13, - "p99_frame_time_ms": 15.95, - "min_frame_time_ms": 4.61, + "total_time_ms": 2104.1, + "avg_fps": 142.6, + "avg_frame_time_ms": 6.99, + "p99_frame_time_ms": 15.98, + "min_frame_time_ms": 5.43, "subsystems": { - "ca_step_avg_us": 1708, - "ca_step_p99_us": 11597, - "ca_step_min_us": 332, - "render_avg_us": 3717, - "render_p99_us": 6538, - "render_min_us": 3295 + "ca_step_avg_us": 2900, + "ca_step_p99_us": 11976, + "ca_step_min_us": 1280, + "render_avg_us": 3459, + "render_p99_us": 5031, + "render_min_us": 3155 } } \ No newline at end of file diff --git a/src/game.rs b/src/game.rs index 0514abf..4a733a2 100644 --- a/src/game.rs +++ b/src/game.rs @@ -4,6 +4,7 @@ use crate::entity::player::Player; use crate::entity::{EntityKind, EntityManager, ItemManager}; use crate::input::{Action, InputHandler}; use crate::physics::collision::resolve_grid_collision; +use crate::physics::particle::ParticleManager; use crate::physics::projectile::{ProjectileManager, ProjectileType}; use crate::physics::verlet::VerletSolver; use crate::render::lighting; @@ -24,6 +25,7 @@ pub struct Game { pub verlet: VerletSolver, pub entities: EntityManager, pub projectiles: ProjectileManager, + pub particles: ParticleManager, pub items: ItemManager, pub player: Player, pub input: InputHandler, @@ -72,6 +74,7 @@ impl Game { verlet: VerletSolver::new(), entities, projectiles: ProjectileManager::new(), + particles: ParticleManager::new(2000), items: ItemManager::new(), player, input: InputHandler::new(), @@ -120,6 +123,7 @@ impl Game { verlet: VerletSolver::new(), entities, projectiles: ProjectileManager::new(), + particles: ParticleManager::new(2000), items: ItemManager::new(), player, input: InputHandler::new(), @@ -548,10 +552,79 @@ impl Game { self.try_spawn_slime(); } + self.spawn_ambient_particles(); self.play_ambient_sounds(); + self.particles.update(); self.grid.swap_modified_flags(); } + fn spawn_ambient_particles(&mut self) { + let (px, py) = self.player.center(&self.entities); + let radius = 30i32; + let mut fire_count = 0u32; + let mut lava_count = 0u32; + let mut water_count = 0u32; + for dy in -radius..=radius { + for dx in -radius..=radius { + let x = px as i32 + dx; + let y = py as i32 + dy; + if !self.grid.in_bounds(x, y) { + continue; + } + let cell = self.grid.get(x, y); + match cell.material { + MaterialId::Fire => { + fire_count += 1; + if self.tick % 3 == 0 + && (dx * dx + dy * dy) < 400 + && self.particles.count() < 1800 + { + let mut rng = + (x as u32).wrapping_mul(7919).wrapping_add(self.tick as u32); + rng ^= rng << 13; + rng ^= rng >> 17; + let off_x = ((rng & 0xFF) as f32 / 255.0 - 0.5) * 2.0; + let off_y = ((rng >> 8 & 0xFF) as f32 / 255.0 - 0.5) * 2.0; + self.particles.spawn( + x as f32 + off_x, + y as f32 + off_y, + off_x * 0.3, + -0.5 - ((rng >> 16) as f32 / 65535.0), + 15 + (rng % 10), + [255, 140 + ((rng >> 4) % 80) as u8, 20, 200], + 1.0 + ((rng >> 8) % 3) as f32 * 0.5, + -0.02, + ); + } + } + MaterialId::Lava => { + lava_count += 1; + if self.tick % 20 == 0 + && (dx * dx + dy * dy) < 300 + && self.particles.count() < 1800 + { + self.particles.spawn_burst( + x as f32, + y as f32, + 2, + [255, 100, 20, 180], + 1.5, + 25, + 1.5, + 0.1, + ); + } + } + MaterialId::Water => { + water_count += 1; + } + _ => {} + } + } + } + let _ = (fire_count, lava_count, water_count); + } + fn play_ambient_sounds(&mut self) { if !self.audio.is_enabled() { return; @@ -1658,4 +1731,48 @@ impl Game { } } } + + pub fn trigger_explosion(&mut self, x: i32, y: i32, radius: i32, damage: f32) { + for dy in -radius..=radius { + for dx in -radius..=radius { + let dist_sq = dx * dx + dy * dy; + if dist_sq > radius * radius { + continue; + } + let bx = x + dx; + let by = y + dy; + if !self.grid.in_bounds(bx, by) { + continue; + } + let cell = self.grid.get(bx, by); + if cell.is_solid() && !cell.is_static() { + self.grid.set(bx, by, crate::world::cell::Cell::empty()); + } else if cell.is_empty() { + let mut fire = crate::world::cell::Cell::new(MaterialId::Fire); + fire.updated_this_tick = true; + self.grid.set(bx, by, fire); + self.grid.set_temp(bx, by, 600.0); + } + self.grid.set_pressure(bx, by, 255); + self.grid.mark_dirty(bx, by); + } + } + self.audio.play("explosion"); + for e in self.entities.all_mut() { + if !e.alive { + continue; + } + let dx = e.cx as i32 - x; + let dy = e.cy as i32 - y; + let dist = ((dx * dx + dy * dy) as f32).sqrt(); + if dist < radius as f32 * 1.5 { + let dmg = (damage * (1.0 - dist / (radius as f32 * 1.5))).max(0.0); + e.take_damage(dmg); + let knockback = (1.0 - dist / (radius as f32 * 1.5)).max(0.0) * 5.0; + let dir = if dx == 0 { 0.0 } else { dx as f32 / dist }; + e.set_horizontal_vel(dir * knockback); + e.set_vertical_vel(-knockback * 0.5); + } + } + } } diff --git a/src/main.rs b/src/main.rs index 7f321a2..acba0fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,6 +73,7 @@ trait GpuRenderer { fn grid_w(&self) -> usize; fn grid_h(&self) -> usize; fn cell_pixel_size(&self) -> u32; + fn upload_particles(&mut self, particles: &verbatim::physics::particle::ParticleManager); } impl GpuRenderer for verbatim::render::vulkan::VulkanRenderer { @@ -102,6 +103,9 @@ impl GpuRenderer for verbatim::render::vulkan::VulkanRenderer { fn cell_pixel_size(&self) -> u32 { verbatim::render::vulkan::VulkanRenderer::cell_pixel_size(self) } + fn upload_particles(&mut self, particles: &verbatim::physics::particle::ParticleManager) { + verbatim::render::vulkan::VulkanRenderer::upload_particles(self, particles); + } } impl GpuRenderer for verbatim::render::graphics::GraphicsRenderer { @@ -131,6 +135,9 @@ impl GpuRenderer for verbatim::render::graphics::GraphicsRenderer { fn cell_pixel_size(&self) -> u32 { verbatim::render::graphics::GraphicsRenderer::cell_pixel_size(self) } + fn upload_particles(&mut self, particles: &verbatim::physics::particle::ParticleManager) { + verbatim::render::graphics::GraphicsRenderer::upload_particles(self, particles); + } } fn main() { @@ -532,6 +539,7 @@ fn run_gpu_mode(title: &str) { game.build_ui(vw, vh); + renderer.upload_particles(&game.particles); renderer.render( &game.grid, &game.entities, diff --git a/src/physics/mod.rs b/src/physics/mod.rs index 5635e2c..f7e8b7f 100644 --- a/src/physics/mod.rs +++ b/src/physics/mod.rs @@ -1,3 +1,4 @@ pub mod collision; +pub mod particle; pub mod projectile; pub mod verlet; diff --git a/src/physics/particle.rs b/src/physics/particle.rs new file mode 100644 index 0000000..d573c24 --- /dev/null +++ b/src/physics/particle.rs @@ -0,0 +1,113 @@ +#[derive(Clone, Copy)] +pub struct Particle { + pub x: f32, + pub y: f32, + pub vx: f32, + pub vy: f32, + pub life: u32, + pub max_life: u32, + pub color: [u8; 4], + pub size: f32, + pub gravity: f32, +} + +impl Particle { + pub fn alive(&self) -> bool { + self.life > 0 + } +} + +pub struct ParticleManager { + particles: Vec, + capacity: usize, +} + +impl ParticleManager { + pub fn new(capacity: usize) -> Self { + Self { + particles: Vec::with_capacity(capacity), + capacity, + } + } + + pub fn spawn( + &mut self, + x: f32, + y: f32, + vx: f32, + vy: f32, + life: u32, + color: [u8; 4], + size: f32, + gravity: f32, + ) { + if self.particles.len() >= self.capacity { + return; + } + self.particles.push(Particle { + x, + y, + vx, + vy, + life, + max_life: life, + color, + size, + gravity, + }); + } + + pub fn spawn_burst( + &mut self, + x: f32, + y: f32, + count: usize, + color: [u8; 4], + speed: f32, + life: u32, + size: f32, + gravity: f32, + ) { + let mut rng = 0x12345u32; + for _ in 0..count { + rng ^= rng << 13; + rng ^= rng >> 17; + rng ^= rng << 5; + let angle = (rng as f32 / u32::MAX as f32) * std::f32::consts::TAU; + let s = speed * (0.3 + 0.7 * ((rng >> 16) as f32 / u16::MAX as f32)); + let vx = angle.cos() * s; + let vy = angle.sin() * s - speed * 0.3; + self.spawn(x, y, vx, vy, life, color, size, gravity); + } + } + + pub fn update(&mut self) { + for p in &mut self.particles { + p.life = p.life.saturating_sub(1); + p.vy += p.gravity; + p.x += p.vx; + p.y += p.vy; + p.vx *= 0.96; + p.vy *= 0.96; + } + self.particles.retain(|p| p.alive()); + } + + pub fn all(&self) -> &[Particle] { + &self.particles + } + + pub fn clear(&mut self) { + self.particles.clear(); + } + + pub fn count(&self) -> usize { + self.particles.len() + } +} + +impl Default for ParticleManager { + fn default() -> Self { + Self::new(2000) + } +} diff --git a/src/render/graphics.rs b/src/render/graphics.rs index a39cf1b..992b8e9 100644 --- a/src/render/graphics.rs +++ b/src/render/graphics.rs @@ -46,6 +46,14 @@ struct ColorInstance { color: [u8; 4], } +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct ParticleInstance { + grid_x: f32, + grid_y: f32, + color: [u8; 4], +} + #[repr(C)] #[derive(bytemuck::NoUninit, Clone, Copy, Default)] struct GpuLightSource { @@ -115,6 +123,12 @@ pub struct GraphicsRenderer { ui_instance_ptr: *mut ColorInstance, ui_instance_capacity: usize, + particle_instance_buffer: vk::Buffer, + particle_instance_memory: vk::DeviceMemory, + particle_instance_ptr: *mut ParticleInstance, + particle_instance_capacity: usize, + particle_count: usize, + grid_buffer: vk::Buffer, grid_memory: vk::DeviceMemory, grid_ptr: *mut u32, @@ -706,6 +720,44 @@ impl GraphicsRenderer { ptr as *mut ColorInstance }; + let part_capacity = 2000usize; + let part_inst_sz = + (part_capacity * std::mem::size_of::()) as vk::DeviceSize; + let pibi = vk::BufferCreateInfo::default() + .size(part_inst_sz) + .usage(vk::BufferUsageFlags::VERTEX_BUFFER) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + let particle_instance_buffer = unsafe { device.create_buffer(&pibi, None) } + .map_err(|e| format!("part inst buf: {e:?}"))?; + let preq = unsafe { device.get_buffer_memory_requirements(particle_instance_buffer) }; + let pmt = find_mem( + preq.memory_type_bits, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + let particle_instance_memory = unsafe { + device.allocate_memory( + &vk::MemoryAllocateInfo::default() + .allocation_size(preq.size) + .memory_type_index(pmt), + None, + ) + } + .map_err(|e| format!("part inst mem: {e:?}"))?; + let particle_instance_ptr = unsafe { + device + .bind_buffer_memory(particle_instance_buffer, particle_instance_memory, 0) + .expect("bind part inst"); + let ptr = device + .map_memory( + particle_instance_memory, + 0, + part_inst_sz, + vk::MemoryMapFlags::default(), + ) + .expect("map part inst"); + ptr as *mut ParticleInstance + }; + Ok(Self { grid_w, grid_h, @@ -742,6 +794,11 @@ impl GraphicsRenderer { ui_instance_memory, ui_instance_ptr, ui_instance_capacity: ui_capacity, + particle_instance_buffer, + particle_instance_memory, + particle_instance_ptr, + particle_instance_capacity: part_capacity, + particle_count: 0, grid_buffer, grid_memory, @@ -1076,6 +1133,35 @@ impl GraphicsRenderer { device.cmd_draw_indexed(cmd, 6, ui_count as u32, 0, 0, 0); } + if self.particle_count > 0 { + device.cmd_bind_vertex_buffers( + cmd, + 0, + &[self.vertex_buffer, self.particle_instance_buffer], + &[0, 0], + ); + let part_pc = PushConstants { + screen_size: [ + self.swapchain_extent.width as f32, + self.swapchain_extent.height as f32, + ], + cell_size: [CHAR_W as f32, CHAR_H as f32], + world_size: [self.grid_w as i32, self.grid_h as i32], + cam_pos: [cam_x, cam_y], + ambient: [1.0, 1.0, 1.0], + is_ui: 2, + light_count: 0, + }; + device.cmd_push_constants( + cmd, + self.pipeline_layout, + vk::ShaderStageFlags::VERTEX, + 0, + bytemuck::bytes_of(&part_pc), + ); + device.cmd_draw_indexed(cmd, 6, self.particle_count as u32, 0, 0, 0); + } + device.cmd_end_render_pass(cmd); let _ = device.end_command_buffer(cmd); @@ -1115,6 +1201,29 @@ impl GraphicsRenderer { CHAR_W } + pub fn upload_particles(&mut self, particles: &crate::physics::particle::ParticleManager) { + let instances = unsafe { + std::slice::from_raw_parts_mut( + self.particle_instance_ptr, + self.particle_instance_capacity, + ) + }; + let mut count = 0usize; + for p in particles.all() { + if count >= self.particle_instance_capacity { + break; + } + let alpha = ((p.life as f32 / p.max_life as f32) * 255.0) as u8; + instances[count] = ParticleInstance { + grid_x: p.x, + grid_y: p.y, + color: [p.color[0], p.color[1], p.color[2], alpha], + }; + count += 1; + } + self.particle_count = count; + } + fn check_resize(&mut self) { let sl = ash::khr::surface::Instance::new(&self.entry, &self.instance); let caps = match unsafe { @@ -1346,6 +1455,9 @@ impl Drop for GraphicsRenderer { self.device.free_memory(self.instance_memory, None); self.device.destroy_buffer(self.ui_instance_buffer, None); self.device.free_memory(self.ui_instance_memory, None); + self.device + .destroy_buffer(self.particle_instance_buffer, None); + self.device.free_memory(self.particle_instance_memory, None); self.device.destroy_buffer(self.vertex_buffer, None); self.device.free_memory(self.vertex_memory, None); self.device.destroy_buffer(self.index_buffer, None); diff --git a/src/render/vulkan.rs b/src/render/vulkan.rs index aad67bc..5cea7ef 100644 --- a/src/render/vulkan.rs +++ b/src/render/vulkan.rs @@ -712,6 +712,8 @@ impl VulkanRenderer { CHAR_W } + pub fn upload_particles(&mut self, _particles: &crate::physics::particle::ParticleManager) {} + fn check_resize(&mut self) { let sl = ash::khr::surface::Instance::new(&self.entry, &self.instance); let caps = match unsafe { diff --git a/src/world/cellular.rs b/src/world/cellular.rs index a15138b..0159451 100644 --- a/src/world/cellular.rs +++ b/src/world/cellular.rs @@ -146,6 +146,8 @@ impl CellularAutomaton { self.heat_transfer(grid, &active, &pre_dirty); self.gas_step(grid, &active, &pre_dirty); self.pressure_step(grid, &active, &pre_dirty); + self.electricity_step(grid, &active, &pre_dirty); + self.structural_step(grid, &active); self.light_step(grid, &active); self.tick += 1; } @@ -760,6 +762,172 @@ impl CellularAutomaton { } } + fn electricity_step( + &mut self, + grid: &mut ChunkedGrid, + active: &[(i32, i32)], + pre_dirty: &std::collections::HashMap<(i32, i32), (i32, i32, i32, i32)>, + ) { + let reg = crate::world::material::MaterialRegistry::instance(); + let cs = CHUNK_SIZE as i32; + for &(cx, cy) in active { + let dirty = grid.get_chunk_dirty(cx, cy); + let pre = pre_dirty.get(&(cx, cy)).copied(); + let (min_x, min_y, max_x, max_y) = match (dirty, pre) { + (Some(d), Some(p)) => (d.0.min(p.0), d.1.min(p.1), d.2.max(p.2), d.3.max(p.3)), + (Some(d), None) => d, + (None, Some(p)) => p, + (None, None) => continue, + }; + let w = max_x - min_x + 1; + let h = max_y - min_y + 1; + let ox = cx * cs; + let oy = cy * cs; + let chunk = match grid.get_chunk_mut(cx, cy) { + Some(c) => c, + None => continue, + }; + let has_elec = chunk.electricity.iter().any(|&e| e > 0); + if !has_elec { + continue; + } + let mut changes: Vec<(usize, u8)> = Vec::new(); + for ly in 0..h { + let wy = min_y + ly - oy; + if wy < 0 || wy >= cs { + continue; + } + for lx in 0..w { + let wx = min_x + lx - ox; + if wx < 0 || wx >= cs { + continue; + } + let idx = (wy as usize) * CHUNK_SIZE + (wx as usize); + let cur = chunk.electricity[idx]; + if cur == 0 { + continue; + } + let mat = reg.get(chunk.cells[idx].material); + if !mat.conductive && cur < 200 { + changes.push((idx, cur.saturating_sub(20))); + continue; + } + let mut spread = 0u8; + for &(dx, dy) in &NEIGHBORS4 { + let nx = wx + dx; + let ny = wy + dy; + if nx < 0 || nx >= cs || ny < 0 || ny >= cs { + continue; + } + let ni = (ny as usize) * CHUNK_SIZE + (nx as usize); + let n_mat = reg.get(chunk.cells[ni].material); + let n_elec = chunk.electricity[ni]; + if n_mat.conductive && n_elec < cur { + let diff = (cur - n_elec) / 4; + if diff > 0 { + changes.push((ni, n_elec + diff)); + spread += diff; + } + } + } + if spread > 0 { + changes.push((idx, cur.saturating_sub(spread))); + } else { + changes.push((idx, cur.saturating_sub(5))); + } + } + } + for (idx, val) in changes { + chunk.electricity[idx] = val; + } + } + } + + fn structural_step(&mut self, grid: &mut ChunkedGrid, active: &[(i32, i32)]) { + if !grid.is_infinite() { + return; + } + if self.tick % 30 != 0 { + return; + } + let reg = crate::world::material::MaterialRegistry::instance(); + let cs = CHUNK_SIZE as i32; + for &(cx, cy) in active { + let dirty = match grid.get_chunk_dirty(cx, cy) { + Some(d) => d, + None => continue, + }; + let (min_x, min_y, max_x, max_y) = dirty; + let ox = cx * cs; + let oy = cy * cs; + let chunk = match grid.get_chunk_mut(cx, cy) { + Some(c) => c, + None => continue, + }; + let mut to_collapse: Vec<(usize, i32, i32)> = Vec::new(); + for ly in 0..(max_y - min_y + 1) { + let wy = min_y + ly - oy; + if wy < 0 || wy >= cs { + continue; + } + for lx in 0..(max_x - min_x + 1) { + let wx = min_x + lx - ox; + if wx < 0 || wx >= cs { + continue; + } + let idx = (wy as usize) * CHUNK_SIZE + (wx as usize); + let mat = reg.get(chunk.cells[idx].material); + if !mat.structural { + continue; + } + let below = if wy + 1 < cs { + let bi = ((wy + 1) as usize) * CHUNK_SIZE + (wx as usize); + chunk.cells[bi].is_solid() + } else { + true + }; + if below { + continue; + } + let below_left = if wy + 1 < cs && wx > 0 { + let bi = ((wy + 1) as usize) * CHUNK_SIZE + ((wx - 1) as usize); + chunk.cells[bi].is_solid() + } else { + true + }; + let below_right = if wy + 1 < cs && wx + 1 < cs { + let bi = ((wy + 1) as usize) * CHUNK_SIZE + ((wx + 1) as usize); + chunk.cells[bi].is_solid() + } else { + true + }; + if below_left || below_right { + continue; + } + let left = if wx > 0 { + chunk.cells[(wy as usize) * CHUNK_SIZE + ((wx - 1) as usize)].is_solid() + } else { + true + }; + let right = if wx + 1 < cs { + chunk.cells[(wy as usize) * CHUNK_SIZE + ((wx + 1) as usize)].is_solid() + } else { + true + }; + if !left && !right { + to_collapse.push((idx, wx, wy)); + } + } + } + for (idx, wx, wy) in to_collapse { + chunk.cells[idx] = Cell::new(MaterialId::Sand); + chunk.temps[idx] = 20.0; + chunk.modified = true; + chunk.mark_dirty(wx, wy); + } + } + } + fn light_step(&mut self, grid: &mut ChunkedGrid, active: &[(i32, i32)]) { self.light_tick += 1; if self.light_tick % 20 != 0 { diff --git a/src/world/chunk.rs b/src/world/chunk.rs index da9766b..1bc719d 100644 --- a/src/world/chunk.rs +++ b/src/world/chunk.rs @@ -9,6 +9,7 @@ pub struct Chunk { pub gas_type: Vec, pub gas_density: Vec, pub light: Vec<[u8; 3]>, + pub electricity: Vec, pub active: bool, pub modified: bool, pub was_modified: bool, @@ -28,6 +29,7 @@ impl Chunk { gas_type: vec![0; CHUNK_AREA], gas_density: vec![0; CHUNK_AREA], light: vec![[0, 0, 0]; CHUNK_AREA], + electricity: vec![0; CHUNK_AREA], active: false, modified: false, was_modified: false, @@ -142,6 +144,21 @@ impl Chunk { } } + #[inline] + pub fn get_electricity(&self, x: i32, y: i32) -> u8 { + if !Self::in_bounds(x, y) { + return 0; + } + self.electricity[Self::idx(x, y)] + } + + #[inline] + pub fn set_electricity(&mut self, x: i32, y: i32, val: u8) { + if Self::in_bounds(x, y) { + self.electricity[Self::idx(x, y)] = val; + } + } + pub fn is_empty(&self) -> bool { self.cells.iter().all(|c| c.is_empty()) } diff --git a/src/world/chunked_grid.rs b/src/world/chunked_grid.rs index ecd8881..14eab61 100644 --- a/src/world/chunked_grid.rs +++ b/src/world/chunked_grid.rs @@ -450,6 +450,7 @@ impl ChunkedGrid { chunk.pressure.swap(ci1, ci2); chunk.gas_type.swap(ci1, ci2); chunk.gas_density.swap(ci1, ci2); + chunk.electricity.swap(ci1, ci2); chunk.cells[ci2].updated_this_tick = true; chunk.modified = true; chunk.mark_dirty(lx1, ly1); @@ -503,6 +504,7 @@ impl ChunkedGrid { chunk.pressure.swap(i1, i2); chunk.gas_type.swap(i1, i2); chunk.gas_density.swap(i1, i2); + chunk.electricity.swap(i1, i2); chunk.cells[i2].updated_this_tick = true; chunk.modified = true; chunk.mark_dirty(lx1, ly1); @@ -860,6 +862,9 @@ impl ChunkedGrid { for l in &chunk.light { bytes.extend_from_slice(&l[..]); } + for &e in &chunk.electricity { + bytes.push(e); + } let dir = Path::new(path); if let Some(parent) = dir.parent() { std::fs::create_dir_all(parent)?; @@ -919,6 +924,13 @@ impl ChunkedGrid { chunk.light[i] = [data[off], data[off + 1], data[off + 2]]; off += 3; } + for i in 0..area { + if off >= data.len() { + break; + } + chunk.electricity[i] = data[off]; + off += 1; + } } else { let (x0, y0, x1, y1) = bounds; let w = (x1 - x0) as usize; diff --git a/src/world/material.rs b/src/world/material.rs index bad564e..4f57422 100644 --- a/src/world/material.rs +++ b/src/world/material.rs @@ -13,6 +13,9 @@ pub struct Material { pub ignition_temp: f32, pub melt_temp: f32, pub heat_conductivity: f32, + pub conductive: bool, + pub conductivity: f32, + pub structural: bool, pub color_fg: (u8, u8, u8), pub color_bg: (u8, u8, u8), pub display_char: char, @@ -40,6 +43,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.0, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (15, 15, 20), color_bg: (10, 10, 15), display_char: ' ', @@ -56,6 +62,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: 1700.0, heat_conductivity: 0.2, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (218, 178, 90), color_bg: (60, 50, 30), display_char: '.', @@ -72,6 +81,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: 0.0, heat_conductivity: 0.4, + conductive: true, + conductivity: 0.6, + structural: false, color_fg: (64, 128, 220), color_bg: (20, 40, 80), display_char: '~', @@ -88,6 +100,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: 1200.0, heat_conductivity: 0.3, + conductive: false, + conductivity: 0.0, + structural: true, color_fg: (120, 120, 130), color_bg: (40, 40, 50), display_char: '#', @@ -104,6 +119,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.05, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (255, 80, 20), color_bg: (120, 20, 0), display_char: '#', @@ -120,6 +138,9 @@ impl MaterialRegistry { ignition_temp: 300.0, melt_temp: f32::INFINITY, heat_conductivity: 0.1, + conductive: false, + conductivity: 0.0, + structural: true, color_fg: (140, 90, 50), color_bg: (50, 30, 20), display_char: 'T', @@ -136,6 +157,9 @@ impl MaterialRegistry { ignition_temp: 200.0, melt_temp: f32::INFINITY, heat_conductivity: 0.15, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (180, 50, 50), color_bg: (60, 15, 15), display_char: '%', @@ -152,6 +176,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.1, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (220, 210, 190), color_bg: (60, 55, 50), display_char: '`', @@ -168,6 +195,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.2, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (200, 200, 220), color_bg: (30, 30, 40), display_char: '~', @@ -184,6 +214,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.6, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (255, 160, 40), color_bg: (100, 30, 0), display_char: '^', @@ -200,6 +233,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.3, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (100, 255, 60), color_bg: (20, 60, 10), display_char: '~', @@ -216,6 +252,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.1, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (100, 100, 100), color_bg: (20, 20, 20), display_char: '*', @@ -232,6 +271,9 @@ impl MaterialRegistry { ignition_temp: 250.0, melt_temp: f32::INFINITY, heat_conductivity: 0.1, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (80, 200, 60), color_bg: (20, 50, 15), display_char: '"', @@ -248,6 +290,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.2, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (100, 70, 50), color_bg: (40, 30, 20), display_char: ':', @@ -264,6 +309,9 @@ impl MaterialRegistry { ignition_temp: f32::INFINITY, melt_temp: f32::INFINITY, heat_conductivity: 0.0, + conductive: false, + conductivity: 0.0, + structural: false, color_fg: (255, 220, 80), color_bg: (60, 50, 20), display_char: '>',