Files
Verbatim/src/physics/particle.rs
T
Emil 0d54d987ac 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.
2026-06-22 20:13:07 +03:00

114 lines
2.3 KiB
Rust

#[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)
}
}