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:
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
pub mod collision;
|
||||
pub mod particle;
|
||||
pub mod projectile;
|
||||
pub mod verlet;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '>',
|
||||
|
||||
Reference in New Issue
Block a user