feat: explosions, particle system, electricity, structural integrity

Explosions:
- trigger_explosion(x, y, radius, damage) in Game
- Destroys non-static solids, ignites empty cells, pressure spike
- Knockback to entities (velocity away from center + upward pop)
- Particle burst on explosion (fire + smoke + debris)

Particle system:
- ParticleManager in src/physics/particle.rs (capacity 2000)
- CPU-side: position, velocity, life, color, gravity, size
- spawn_burst() for radial bursts, individual spawn() for ambient
- Rendered as third draw call in graphics.rs (is_ui: 2 in shader)
- ParticleInstance = ColorInstance compatible (12 bytes)
- upload_particles() via GpuRenderer trait
- Ambient: fire sparks, lava bubbles — scanned around player

Electricity:
- chunk.electricity: Vec<u8> (4KB/chunk) — current strength 0-255
- Material.conductive + conductivity fields (water = conductive)
- electricity_step() in CA: propagates current through conductive
  neighbors, decays over time, skips non-conductive cells
- cells_swap swaps electricity, serialization saves/loads it

Structural integrity:
- Material.structural: bool (stone, wood = true)
- structural_step() in CA (every 30 ticks, infinite mode only):
  checks if structural cells have support (below, below-left, below-right)
  converts unsupported to Sand (falls)
- Only active in infinite mode to avoid breaking test grids

Graphics shader: is_ui==2 branch renders particles in world-space
All 185 tests + 14 scenarios pass. 143 FPS benchmark.
This commit is contained in:
Emil
2026-06-22 20:13:07 +03:00
parent 4a89612081
commit 0d54d987ac
13 changed files with 631 additions and 21 deletions
+22 -10
View File
@@ -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);
}
}
}
Binary file not shown.
+11 -11
View File
@@ -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
}
}
+117
View File
@@ -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);
}
}
}
}
+8
View File
@@ -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<R: GpuRenderer>(title: &str) {
game.build_ui(vw, vh);
renderer.upload_particles(&game.particles);
renderer.render(
&game.grid,
&game.entities,
+1
View File
@@ -1,3 +1,4 @@
pub mod collision;
pub mod particle;
pub mod projectile;
pub mod verlet;
+113
View File
@@ -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<Particle>,
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)
}
}
+112
View File
@@ -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::<ParticleInstance>()) 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);
+2
View File
@@ -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 {
+168
View File
@@ -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 {
+17
View File
@@ -9,6 +9,7 @@ pub struct Chunk {
pub gas_type: Vec<u8>,
pub gas_density: Vec<u8>,
pub light: Vec<[u8; 3]>,
pub electricity: Vec<u8>,
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())
}
+12
View File
@@ -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;
+48
View File
@@ -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: '>',