feat: multi-layer world — temperature, gas, pressure, light as parallel per-chunk arrays
- Removed temp from Cell (13→9 bytes), added temps/pressure/gas_type/gas_density/light arrays to Chunk - Layer access via grid.get_temp()/set_temp()/get_gas()/set_gas()/get_pressure()/set_pressure()/get_light()/set_light() - cells_swap swaps all layers, set_material sets default_temp - heat_transfer refactored to direct array access on temps[] (no Cell copy) - CA rules refactored: cell.temp → grid.get_temp()/set_temp() - gas_step: gas flow (rise, spread), fire produces CO2+smoke, steam condenses to water, acid+organic→poison gas - pressure_step: pressure equalization for connected non-solid cells - light_step: world-space persistent lighting, updated every 10 ticks, ray-cast line-of-sight - Gas damage: poison gas damages entities, CO2 suffocates, applied before ca.step() - Multi-section chunk serialization (VWM1 magic + cells + temps + gas + pressure + light) - Old 12-byte chunk format auto-detected for backward compat - AI spectrum: new gas + pressure spectrums, light spectrum uses world-space fallback - Protocol: gas/pressure spectrum commands - pre_dirty mechanism: layer steps process pre-clear dirty rects for cross-cell diffusion - 14 new multilayer tests, all 185 tests + 14 scenarios pass - Benchmark: 85.9 FPS (graphics surface, was 128 pre-layers — 33% regression from 4 new layer steps)
This commit is contained in:
@@ -289,6 +289,8 @@ fn handle_command(cmd: Command, session: &mut Option<GameSession>) -> Response {
|
||||
"entities" => crate::ai::spectrum::Spectrum::Entities,
|
||||
"density" => crate::ai::spectrum::Spectrum::Density,
|
||||
"velocity" => crate::ai::spectrum::Spectrum::Velocity,
|
||||
"gas" => crate::ai::spectrum::Spectrum::Gas,
|
||||
"pressure" => crate::ai::spectrum::Spectrum::Pressure,
|
||||
_ => {
|
||||
return Response::err(
|
||||
"Unknown spectrum. Use: materials, temperature, light, entities, density, velocity",
|
||||
|
||||
+6
-6
@@ -155,26 +155,26 @@ fn check_assertion(session: &GameSession, assertion: &Assertion) -> AssertionRes
|
||||
}
|
||||
}
|
||||
Assertion::CellTempGreaterThan { x, y, temp } => {
|
||||
let cell = session.get_cell(*x, *y);
|
||||
let passed = cell.temp > *temp;
|
||||
let cell_temp = session.get_temp(*x, *y);
|
||||
let passed = cell_temp > *temp;
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed,
|
||||
message: format!(
|
||||
"Cell({},{}) temp = {:.1}, expected > {:.1}",
|
||||
x, y, cell.temp, temp
|
||||
x, y, cell_temp, temp
|
||||
),
|
||||
}
|
||||
}
|
||||
Assertion::CellTempLessThan { x, y, temp } => {
|
||||
let cell = session.get_cell(*x, *y);
|
||||
let passed = cell.temp < *temp;
|
||||
let cell_temp = session.get_temp(*x, *y);
|
||||
let passed = cell_temp < *temp;
|
||||
AssertionResult {
|
||||
assertion: assertion.clone(),
|
||||
passed,
|
||||
message: format!(
|
||||
"Cell({},{}) temp = {:.1}, expected < {:.1}",
|
||||
x, y, cell.temp, temp
|
||||
x, y, cell_temp, temp
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -133,6 +133,10 @@ impl GameSession {
|
||||
CellInfo::from_grid(&self.game.grid, x, y)
|
||||
}
|
||||
|
||||
pub fn get_temp(&self, x: i32, y: i32) -> f32 {
|
||||
self.game.grid.get_temp(x, y)
|
||||
}
|
||||
|
||||
pub fn get_region(&self, x: i32, y: i32, w: i32, h: i32) -> Vec<CellInfo> {
|
||||
let mut cells = Vec::with_capacity((w * h) as usize);
|
||||
for dy in 0..h {
|
||||
@@ -186,8 +190,21 @@ impl GameSession {
|
||||
|
||||
pub fn find_material(&self, material: &str) -> Option<(i32, i32)> {
|
||||
let target = crate::ai::state::material_from_name(material)?;
|
||||
for y in 0..self.game.grid.height as i32 {
|
||||
for x in 0..self.game.grid.width as i32 {
|
||||
let (px, py) = self.game.player.center(&self.game.entities);
|
||||
let radius = if self.game.grid.is_infinite() {
|
||||
200
|
||||
} else {
|
||||
self.game.grid.width as i32
|
||||
};
|
||||
let cx = px as i32;
|
||||
let cy = py as i32;
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let x = cx + dx;
|
||||
let y = cy + dy;
|
||||
if !self.game.grid.in_bounds(x, y) {
|
||||
continue;
|
||||
}
|
||||
if self.game.grid.get(x, y).material == target {
|
||||
return Some((x, y));
|
||||
}
|
||||
|
||||
+76
-1
@@ -8,6 +8,8 @@ pub enum Spectrum {
|
||||
Entities,
|
||||
Density,
|
||||
Velocity,
|
||||
Gas,
|
||||
Pressure,
|
||||
}
|
||||
|
||||
impl Spectrum {
|
||||
@@ -19,6 +21,8 @@ impl Spectrum {
|
||||
Spectrum::Entities => "entities",
|
||||
Spectrum::Density => "density",
|
||||
Spectrum::Velocity => "velocity",
|
||||
Spectrum::Gas => "gas",
|
||||
Spectrum::Pressure => "pressure",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +34,8 @@ impl Spectrum {
|
||||
Spectrum::Entities,
|
||||
Spectrum::Density,
|
||||
Spectrum::Velocity,
|
||||
Spectrum::Gas,
|
||||
Spectrum::Pressure,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -51,6 +57,8 @@ pub fn render_spectrum(
|
||||
Spectrum::Entities => render_entities(grid, entities, cam_x, cam_y, vw, vh),
|
||||
Spectrum::Density => render_density(grid, cam_x, cam_y, vw, vh),
|
||||
Spectrum::Velocity => render_velocity(grid, entities, cam_x, cam_y, vw, vh),
|
||||
Spectrum::Gas => render_gas(grid, cam_x, cam_y, vw, vh),
|
||||
Spectrum::Pressure => render_pressure(grid, cam_x, cam_y, vw, vh),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +121,7 @@ fn render_temperature(grid: &ChunkedGrid, cam_x: i32, cam_y: i32, vw: usize, vh:
|
||||
if cell.is_empty() {
|
||||
buf.push(' ');
|
||||
} else {
|
||||
let t = cell.temp;
|
||||
let t = grid.get_temp(x, y);
|
||||
let ch = if t < 0.0 {
|
||||
'.'
|
||||
} else if t < 20.0 {
|
||||
@@ -158,6 +166,13 @@ fn render_light(
|
||||
let (r, g, b) = if let Some(lg) = light {
|
||||
let c = lg.get(dx as i32, dy as i32);
|
||||
(c[0], c[1], c[2])
|
||||
} else if grid.in_bounds(x, y) {
|
||||
let wl = grid.get_light(x, y);
|
||||
if wl[0] > 0 || wl[1] > 0 || wl[2] > 0 {
|
||||
(wl[0], wl[1], wl[2])
|
||||
} else {
|
||||
(ambient[0], ambient[1], ambient[2])
|
||||
}
|
||||
} else {
|
||||
(ambient[0], ambient[1], ambient[2])
|
||||
};
|
||||
@@ -364,3 +379,63 @@ pub fn format_all_spectrums(
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn render_gas(grid: &ChunkedGrid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
|
||||
let mut buf = String::with_capacity(vw * vh + vh);
|
||||
for dy in 0..vh {
|
||||
for dx in 0..vw {
|
||||
let x = cam_x + dx as i32;
|
||||
let y = cam_y + dy as i32;
|
||||
if !grid.in_bounds(x, y) {
|
||||
buf.push(' ');
|
||||
} else {
|
||||
let (gt, gd) = grid.get_gas(x, y);
|
||||
let ch = if gd == 0 {
|
||||
' '
|
||||
} else {
|
||||
match gt {
|
||||
1 => '.', // smoke
|
||||
2 => 'x', // poison
|
||||
3 => 'o', // CO2
|
||||
4 => '~', // steam
|
||||
_ => '?',
|
||||
}
|
||||
};
|
||||
buf.push(ch);
|
||||
}
|
||||
}
|
||||
buf.push('\n');
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
fn render_pressure(grid: &ChunkedGrid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
|
||||
let mut buf = String::with_capacity(vw * vh + vh);
|
||||
for dy in 0..vh {
|
||||
for dx in 0..vw {
|
||||
let x = cam_x + dx as i32;
|
||||
let y = cam_y + dy as i32;
|
||||
if !grid.in_bounds(x, y) {
|
||||
buf.push(' ');
|
||||
} else {
|
||||
let p = grid.get_pressure(x, y);
|
||||
let ch = if p < 80 {
|
||||
'L'
|
||||
} else if p < 120 {
|
||||
'-'
|
||||
} else if p < 140 {
|
||||
'.'
|
||||
} else if p < 180 {
|
||||
'+'
|
||||
} else if p < 220 {
|
||||
'o'
|
||||
} else {
|
||||
'#'
|
||||
};
|
||||
buf.push(ch);
|
||||
}
|
||||
}
|
||||
buf.push('\n');
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ impl CellInfo {
|
||||
x,
|
||||
y,
|
||||
material: mat.name.to_string(),
|
||||
temp: cell.temp,
|
||||
temp: grid.get_temp(x, y),
|
||||
is_solid: mat.solid,
|
||||
is_liquid: mat.liquid,
|
||||
is_gas: mat.gas,
|
||||
|
||||
+16
@@ -450,6 +450,7 @@ impl Game {
|
||||
|
||||
self.stream_chunks();
|
||||
self.update_active_chunks();
|
||||
self.apply_gas_damage();
|
||||
self.ca.step(&mut self.grid);
|
||||
|
||||
self.update_entities();
|
||||
@@ -490,6 +491,21 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_gas_damage(&mut self) {
|
||||
for e in self.entities.all_mut() {
|
||||
if e.alive {
|
||||
let (ex, ey) = e.center();
|
||||
let (gas_type, gas_density) = self.grid.get_gas(ex as i32, ey as i32);
|
||||
if gas_type == 2 && gas_density > 50 {
|
||||
e.health -= (gas_density as f32 - 50.0) * 0.1;
|
||||
}
|
||||
if gas_type == 3 && gas_density > 100 {
|
||||
e.health -= 2.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status_effects(&mut self) {
|
||||
for e in self.entities.all_mut() {
|
||||
if e.alive {
|
||||
|
||||
@@ -143,8 +143,8 @@ impl Projectile {
|
||||
{
|
||||
let mut ignited = cell;
|
||||
ignited.material = MaterialId::Fire;
|
||||
ignited.temp = 400.0;
|
||||
grid.set(x, y, ignited);
|
||||
grid.set_temp(x, y, 400.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-27
@@ -45,18 +45,26 @@ impl MaterialId {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Cell {
|
||||
pub material: MaterialId,
|
||||
pub temp: f32,
|
||||
pub updated_this_tick: bool,
|
||||
pub variant: u8,
|
||||
pub fg: [u8; 3],
|
||||
pub bg: [u8; 3],
|
||||
}
|
||||
|
||||
pub fn default_temp(material: MaterialId) -> f32 {
|
||||
match material {
|
||||
MaterialId::Lava => 1500.0,
|
||||
MaterialId::Fire => 800.0,
|
||||
MaterialId::Steam => 150.0,
|
||||
MaterialId::Smoke => 120.0,
|
||||
_ => 20.0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
material: MaterialId::Empty,
|
||||
temp: 20.0,
|
||||
updated_this_tick: false,
|
||||
variant: 0,
|
||||
fg: [15, 15, 20],
|
||||
@@ -67,16 +75,8 @@ impl Cell {
|
||||
pub fn new(material: MaterialId) -> Self {
|
||||
let reg = MaterialRegistry::instance();
|
||||
let mat = reg.get(material);
|
||||
let temp = match material {
|
||||
MaterialId::Lava => 1500.0,
|
||||
MaterialId::Fire => 800.0,
|
||||
MaterialId::Steam => 150.0,
|
||||
MaterialId::Smoke => 120.0,
|
||||
_ => 20.0,
|
||||
};
|
||||
Self {
|
||||
material,
|
||||
temp,
|
||||
updated_this_tick: false,
|
||||
variant: rand_u8(),
|
||||
fg: [mat.color_fg.0, mat.color_fg.1, mat.color_fg.2],
|
||||
@@ -117,13 +117,12 @@ impl Cell {
|
||||
self.material.display_char()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 12] {
|
||||
let mut out = [0u8; 12];
|
||||
pub fn to_bytes(&self) -> [u8; 8] {
|
||||
let mut out = [0u8; 8];
|
||||
out[0] = self.material as u8;
|
||||
out[1..5].copy_from_slice(&self.temp.to_le_bytes());
|
||||
out[5] = self.variant;
|
||||
out[6..9].copy_from_slice(&self.fg);
|
||||
out[9..12].copy_from_slice(&self.bg);
|
||||
out[1] = self.variant;
|
||||
out[2..5].copy_from_slice(&self.fg);
|
||||
out[5..8].copy_from_slice(&self.bg);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -150,25 +149,19 @@ impl Cell {
|
||||
_ => MaterialId::Stone,
|
||||
}
|
||||
};
|
||||
let temp = if bytes.len() >= 5 {
|
||||
f32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]])
|
||||
} else {
|
||||
20.0
|
||||
};
|
||||
let variant = bytes.get(5).copied().unwrap_or(0);
|
||||
let fg = if bytes.len() >= 9 {
|
||||
[bytes[6], bytes[7], bytes[8]]
|
||||
let variant = bytes.get(1).copied().unwrap_or(0);
|
||||
let fg = if bytes.len() >= 5 {
|
||||
[bytes[2], bytes[3], bytes[4]]
|
||||
} else {
|
||||
[15, 15, 20]
|
||||
};
|
||||
let bg = if bytes.len() >= 12 {
|
||||
[bytes[9], bytes[10], bytes[11]]
|
||||
let bg = if bytes.len() >= 8 {
|
||||
[bytes[5], bytes[6], bytes[7]]
|
||||
} else {
|
||||
[10, 10, 15]
|
||||
};
|
||||
Self {
|
||||
material,
|
||||
temp,
|
||||
updated_this_tick: false,
|
||||
variant,
|
||||
fg,
|
||||
|
||||
+358
-125
@@ -1,10 +1,12 @@
|
||||
use crate::world::cell::{Cell, MaterialId};
|
||||
use crate::world::cell::{default_temp, Cell, MaterialId};
|
||||
use crate::world::chunk::CHUNK_SIZE;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub struct CellularAutomaton {
|
||||
tick: u64,
|
||||
rng_state: u64,
|
||||
temps: Vec<f32>,
|
||||
temps_buf: Vec<f32>,
|
||||
light_tick: u64,
|
||||
}
|
||||
|
||||
impl CellularAutomaton {
|
||||
@@ -12,7 +14,8 @@ impl CellularAutomaton {
|
||||
Self {
|
||||
tick: 0,
|
||||
rng_state: 0x1234567890ABCDEF,
|
||||
temps: Vec::new(),
|
||||
temps_buf: Vec::new(),
|
||||
light_tick: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +92,31 @@ impl CellularAutomaton {
|
||||
let mut active = grid.active_chunks();
|
||||
active.sort_by(|(ax, ay), (bx, by)| by.cmp(ay).then(bx.cmp(ax)));
|
||||
|
||||
let mut pre_dirty: Vec<((i32, i32), (i32, i32, i32, i32))> = Vec::new();
|
||||
for (cx, cy) in &active {
|
||||
if let Some(d) = grid.get_chunk_dirty(*cx, *cy) {
|
||||
pre_dirty.push(((*cx, *cy), d));
|
||||
}
|
||||
}
|
||||
|
||||
for (cx, cy) in &active {
|
||||
let dirty = grid.get_chunk_dirty(*cx, *cy);
|
||||
if dirty.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (min_x, min_y, max_x, max_y) = dirty.unwrap();
|
||||
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let mut cell = grid.get(x, y);
|
||||
if cell.updated_this_tick {
|
||||
cell.updated_this_tick = false;
|
||||
grid.set(x, y, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (cx, cy) in active {
|
||||
let dirty = grid.get_chunk_dirty(cx, cy);
|
||||
if dirty.is_none() {
|
||||
@@ -98,15 +126,6 @@ impl CellularAutomaton {
|
||||
|
||||
grid.set_chunk_dirty(cx, cy, None);
|
||||
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let mut cell = grid.get(x, y);
|
||||
cell.updated_this_tick = false;
|
||||
grid.set(x, y, cell);
|
||||
}
|
||||
}
|
||||
grid.set_chunk_dirty(cx, cy, None);
|
||||
|
||||
for y in (min_y..=max_y).rev() {
|
||||
if flip {
|
||||
for x in min_x..=max_x {
|
||||
@@ -120,7 +139,10 @@ impl CellularAutomaton {
|
||||
}
|
||||
}
|
||||
|
||||
self.heat_transfer(grid);
|
||||
self.heat_transfer(grid, &pre_dirty);
|
||||
self.gas_step(grid, &pre_dirty);
|
||||
self.pressure_step(grid, &pre_dirty);
|
||||
self.light_step(grid);
|
||||
self.tick += 1;
|
||||
}
|
||||
|
||||
@@ -168,57 +190,56 @@ impl CellularAutomaton {
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < 1.0) {
|
||||
grid.cells_swap(x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
let dl = grid.get(x - dir, y + 1);
|
||||
let dr = grid.get(x + dir, y + 1);
|
||||
|
||||
let can_dl = grid.in_bounds(x - dir, y + 1)
|
||||
&& (dl.is_empty() || (dl.is_liquid() && dl.density() < 1.0));
|
||||
let can_dr = grid.in_bounds(x + dir, y + 1)
|
||||
&& (dr.is_empty() || (dr.is_liquid() && dr.density() < 1.0));
|
||||
|
||||
if can_dl && can_dr {
|
||||
if self.rand_bool() {
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else {
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
}
|
||||
} else if can_dl {
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else if can_dr {
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
} else {
|
||||
let can_l = grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty();
|
||||
let can_r = grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty();
|
||||
if can_l && can_r {
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
let dl = grid.get(x - dir, y + 1);
|
||||
let dr = grid.get(x + dir, y + 1);
|
||||
|
||||
let can_dl = grid.in_bounds(x - dir, y + 1)
|
||||
&& (dl.is_empty() || (dl.is_liquid() && dl.density() < 1.0));
|
||||
let can_dr = grid.in_bounds(x + dir, y + 1)
|
||||
&& (dr.is_empty() || (dr.is_liquid() && dr.density() < 1.0));
|
||||
|
||||
if can_dl && can_dr {
|
||||
if self.rand_bool() {
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else {
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
}
|
||||
} else if can_dl {
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else if can_dr {
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
} else {
|
||||
let can_l = grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty();
|
||||
let can_r = grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty();
|
||||
if can_l && can_r {
|
||||
if self.rand_bool() {
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else {
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
} else if can_l {
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if can_r {
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
} else if can_l {
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if can_r {
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 100.0 {
|
||||
let mut new = cell;
|
||||
let temp = grid.get_temp(x, y);
|
||||
if temp > 100.0 {
|
||||
let mut new = grid.get(x, y);
|
||||
new.material = MaterialId::Steam;
|
||||
new.temp = 110.0;
|
||||
grid.set(x, y, new);
|
||||
grid.set_temp(x, y, 110.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_lava(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 400.0 {
|
||||
let mut new = cell;
|
||||
let temp = grid.get_temp(x, y);
|
||||
if temp < 400.0 {
|
||||
let mut new = grid.get(x, y);
|
||||
new.material = MaterialId::Stone;
|
||||
grid.set(x, y, new);
|
||||
return;
|
||||
@@ -259,20 +280,19 @@ impl CellularAutomaton {
|
||||
continue;
|
||||
}
|
||||
let neighbor = grid.get(nx, ny);
|
||||
let n_temp = grid.get_temp(nx, ny);
|
||||
match neighbor.material {
|
||||
MaterialId::Water => {
|
||||
grid.set(nx, ny, Cell::new(MaterialId::Steam));
|
||||
let lava = grid.get(x, y);
|
||||
let mut new_lava = lava;
|
||||
new_lava.temp -= 50.0;
|
||||
grid.set(x, y, new_lava);
|
||||
grid.set_temp(nx, ny, 150.0);
|
||||
let lava_temp = grid.get_temp(x, y);
|
||||
grid.set_temp(x, y, lava_temp - 50.0);
|
||||
}
|
||||
MaterialId::Wood | MaterialId::Grass | MaterialId::Flesh
|
||||
if neighbor.temp < 300.0 =>
|
||||
{
|
||||
MaterialId::Wood | MaterialId::Grass | MaterialId::Flesh if n_temp < 300.0 => {
|
||||
grid.set(nx, ny, Cell::new(MaterialId::Fire));
|
||||
grid.set_temp(nx, ny, 400.0);
|
||||
}
|
||||
MaterialId::Sand if neighbor.temp > 1700.0 => {
|
||||
MaterialId::Sand if n_temp > 1700.0 => {
|
||||
grid.set(nx, ny, Cell::new(MaterialId::Stone));
|
||||
}
|
||||
_ => {}
|
||||
@@ -281,12 +301,12 @@ impl CellularAutomaton {
|
||||
}
|
||||
|
||||
fn update_steam(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 80.0 {
|
||||
let mut new = cell;
|
||||
let temp = grid.get_temp(x, y);
|
||||
if temp < 80.0 {
|
||||
let mut new = grid.get(x, y);
|
||||
new.material = MaterialId::Water;
|
||||
new.temp = 50.0;
|
||||
grid.set(x, y, new);
|
||||
grid.set_temp(x, y, 50.0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -316,6 +336,21 @@ impl CellularAutomaton {
|
||||
|
||||
fn update_fire(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
let temp = grid.get_temp(x, y);
|
||||
|
||||
let (egt, egd) = grid.get_gas(x, y);
|
||||
if egd < 200 {
|
||||
grid.set_gas(x, y, 3, (egd + 5).min(255));
|
||||
}
|
||||
if y > 0 {
|
||||
let above = grid.get(x, y - 1);
|
||||
if above.is_empty() {
|
||||
let (agt, agd) = grid.get_gas(x, y - 1);
|
||||
if agt == 0 && agd < 100 {
|
||||
grid.set_gas(x, y - 1, 1, (agd + 3).min(200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
@@ -326,26 +361,27 @@ impl CellularAutomaton {
|
||||
let neighbor = grid.get(nx, ny);
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let mat = reg.get(neighbor.material);
|
||||
if mat.flammable && neighbor.temp < mat.ignition_temp {
|
||||
let n_temp = grid.get_temp(nx, ny);
|
||||
if mat.flammable && n_temp < mat.ignition_temp {
|
||||
let mut new_n = neighbor;
|
||||
new_n.material = MaterialId::Fire;
|
||||
new_n.temp = 400.0;
|
||||
grid.set(nx, ny, new_n);
|
||||
grid.set_temp(nx, ny, 400.0);
|
||||
}
|
||||
}
|
||||
|
||||
if cell.temp < 100.0 || self.rand() % 20 == 0 {
|
||||
let (_gt, gd) = grid.get_gas(x, y);
|
||||
if temp < 100.0 || self.rand() % 20 == 0 || gd > 150 {
|
||||
if self.rand() % 3 == 0 {
|
||||
grid.set(x, y, Cell::new(MaterialId::Smoke));
|
||||
grid.set_temp(x, y, 120.0);
|
||||
} else {
|
||||
grid.set(x, y, Cell::empty());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new = cell;
|
||||
new.temp -= 15.0;
|
||||
grid.set(x, y, new);
|
||||
grid.set_temp(x, y, temp - 15.0);
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() && self.rand() % 2 == 0 {
|
||||
grid.cells_swap(x, y, x, y - 1);
|
||||
@@ -388,6 +424,12 @@ impl CellularAutomaton {
|
||||
&& neighbor.material != MaterialId::Stone
|
||||
&& self.rand() % 4 == 0
|
||||
{
|
||||
if neighbor.material == MaterialId::Flesh || neighbor.material == MaterialId::Wood {
|
||||
let (gt, _gd) = grid.get_gas(nx, ny);
|
||||
if gt == 0 {
|
||||
grid.set_gas(nx, ny, 2, 80);
|
||||
}
|
||||
}
|
||||
grid.set(nx, ny, Cell::empty());
|
||||
if self.rand() % 2 == 0 {
|
||||
grid.set(x, y, Cell::empty());
|
||||
@@ -415,77 +457,55 @@ impl CellularAutomaton {
|
||||
}
|
||||
|
||||
fn update_flesh(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 200.0 {
|
||||
let mut new = cell;
|
||||
let temp = grid.get_temp(x, y);
|
||||
if temp > 200.0 {
|
||||
let mut new = grid.get(x, y);
|
||||
new.material = MaterialId::Fire;
|
||||
new.temp = 400.0;
|
||||
grid.set(x, y, new);
|
||||
grid.set_temp(x, y, 400.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_grass(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 250.0 {
|
||||
let temp = grid.get_temp(x, y);
|
||||
if temp > 250.0 {
|
||||
grid.set(x, y, Cell::new(MaterialId::Fire));
|
||||
grid.set_temp(x, y, 400.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dirt(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 0.0 {
|
||||
let mut new = cell;
|
||||
let temp = grid.get_temp(x, y);
|
||||
if temp < 0.0 {
|
||||
let mut new = grid.get(x, y);
|
||||
new.material = MaterialId::Stone;
|
||||
grid.set(x, y, new);
|
||||
}
|
||||
}
|
||||
|
||||
fn heat_transfer(&mut self, grid: &mut ChunkedGrid) {
|
||||
fn heat_transfer(
|
||||
&mut self,
|
||||
grid: &mut ChunkedGrid,
|
||||
pre_dirty: &[((i32, i32), (i32, i32, i32, i32))],
|
||||
) {
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let gw = grid.width as i32;
|
||||
let gh = grid.height as i32;
|
||||
|
||||
let active = grid.active_chunks();
|
||||
for (cx, cy) in active {
|
||||
let dirty = grid.get_chunk_dirty(cx, cy);
|
||||
if dirty.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (min_x, min_y, max_x, max_y) = dirty.unwrap();
|
||||
|
||||
let ex_min_x = (min_x - 1).max(0);
|
||||
let ex_min_y = (min_y - 1).max(0);
|
||||
let (ex_max_x, ex_max_y) = if grid.is_infinite() {
|
||||
((max_x + 1), (max_y + 1))
|
||||
} else {
|
||||
((max_x + 1).min(gw - 1), (max_y + 1).min(gh - 1))
|
||||
let pre = pre_dirty
|
||||
.iter()
|
||||
.find(|(c, _)| *c == (cx, cy))
|
||||
.map(|(_, d)| *d);
|
||||
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 ew = (ex_max_x - ex_min_x + 1) as usize;
|
||||
let eh = (ex_max_y - ex_min_y + 1) as usize;
|
||||
let ecount = ew.saturating_mul(eh);
|
||||
if ecount > 10000 {
|
||||
eprintln!(
|
||||
"heat_transfer skipping huge dirty rect: chunk=({}, {}) dirty=({},{},{},{}) ew={} eh={}",
|
||||
cx, cy, min_x, min_y, max_x, max_y, ew, eh
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if self.temps.len() < ecount {
|
||||
self.temps.resize(ecount, 0.0);
|
||||
}
|
||||
|
||||
for y in ex_min_y..=ex_max_y {
|
||||
for x in ex_min_x..=ex_max_x {
|
||||
let idx = ((y - ex_min_y) as usize) * ew + (x - ex_min_x) as usize;
|
||||
self.temps[idx] = grid.get(x, y).temp;
|
||||
}
|
||||
}
|
||||
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.is_empty() || cell.is_static() {
|
||||
if cell.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mat = reg.get(cell.material);
|
||||
@@ -493,30 +513,243 @@ impl CellularAutomaton {
|
||||
if k == 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cur_temp = grid.get_temp(x, y);
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0;
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if nx < ex_min_x || nx > ex_max_x || ny < ex_min_y || ny > ex_max_y {
|
||||
continue;
|
||||
}
|
||||
let ni = ((ny - ex_min_y) as usize) * ew + (nx - ex_min_x) as usize;
|
||||
sum += self.temps[ni];
|
||||
sum += grid.get_temp(nx, ny);
|
||||
count += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
let avg = sum / count as f32;
|
||||
let mut new = cell;
|
||||
new.temp += (avg - cell.temp) * k * 0.1;
|
||||
grid.set(x, y, new);
|
||||
grid.mark_dirty(x, y);
|
||||
let new_temp = cur_temp + (avg - cur_temp) * k * 0.1;
|
||||
if (new_temp - cur_temp).abs() > 0.01 {
|
||||
grid.set_temp(x, y, new_temp);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gas_step(
|
||||
&mut self,
|
||||
grid: &mut ChunkedGrid,
|
||||
pre_dirty: &[((i32, i32), (i32, i32, i32, i32))],
|
||||
) {
|
||||
let active = grid.active_chunks();
|
||||
for (cx, cy) in active {
|
||||
let dirty = grid.get_chunk_dirty(cx, cy);
|
||||
let pre = pre_dirty
|
||||
.iter()
|
||||
.find(|(c, _)| *c == (cx, cy))
|
||||
.map(|(_, d)| *d);
|
||||
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,
|
||||
};
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let (gt, gd) = grid.get_gas(x, y);
|
||||
if gd == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if gt == 4 && grid.get_temp(x, y) < 80.0 {
|
||||
let mut new = grid.get(x, y);
|
||||
new.material = MaterialId::Water;
|
||||
grid.set(x, y, new);
|
||||
grid.set_temp(x, y, 50.0);
|
||||
grid.set_gas(x, y, 0, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if y > 0 {
|
||||
let above = grid.get(x, y - 1);
|
||||
if above.is_empty() || above.is_gas() {
|
||||
let (agt, agd) = grid.get_gas(x, y - 1);
|
||||
if agd < gd {
|
||||
grid.set_gas(x, y - 1, gt, gd);
|
||||
grid.set_gas(x, y, agt, agd);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
for &d in &[dir, -dir] {
|
||||
if grid.in_bounds(x + d, y) {
|
||||
let side = grid.get(x + d, y);
|
||||
if side.is_empty() || side.is_gas() {
|
||||
let (sgt, sgd) = grid.get_gas(x + d, y);
|
||||
if sgd < gd.saturating_sub(5) {
|
||||
let avg = (gd + sgd) / 2;
|
||||
grid.set_gas(x + d, y, gt, avg);
|
||||
grid.set_gas(x, y, gt, gd.saturating_sub(avg - sgd));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gt == 1 || gt == 2) && gd > 0 && self.rand() % 120 == 0 {
|
||||
grid.set_gas(x, y, gt, gd.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pressure_step(
|
||||
&mut self,
|
||||
grid: &mut ChunkedGrid,
|
||||
pre_dirty: &[((i32, i32), (i32, i32, i32, i32))],
|
||||
) {
|
||||
let active = grid.active_chunks();
|
||||
for (cx, cy) in active {
|
||||
let dirty = grid.get_chunk_dirty(cx, cy);
|
||||
let pre = pre_dirty
|
||||
.iter()
|
||||
.find(|(c, _)| *c == (cx, cy))
|
||||
.map(|(_, d)| *d);
|
||||
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,
|
||||
};
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.is_solid() {
|
||||
continue;
|
||||
}
|
||||
let cur_p = grid.get_pressure(x, y) as i32;
|
||||
let mut sum = 0i32;
|
||||
let mut count = 0i32;
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if !grid.in_bounds(nx, ny) {
|
||||
continue;
|
||||
}
|
||||
let n = grid.get(nx, ny);
|
||||
if n.is_solid() {
|
||||
continue;
|
||||
}
|
||||
sum += grid.get_pressure(nx, ny) as i32;
|
||||
count += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
let avg = sum / count;
|
||||
let new_p = cur_p + (avg - cur_p) / 8;
|
||||
if new_p != cur_p {
|
||||
grid.set_pressure(x, y, new_p.clamp(0, 255) as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn light_step(&mut self, grid: &mut ChunkedGrid) {
|
||||
self.light_tick += 1;
|
||||
if self.light_tick % 10 != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let active = grid.active_chunks();
|
||||
let cs = CHUNK_SIZE as i32;
|
||||
|
||||
for &(cx, cy) in &active {
|
||||
if let Some(chunk) = grid.get_chunk_mut(cx, cy) {
|
||||
for l in chunk.light.iter_mut() {
|
||||
*l = [0, 0, 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &(cx, cy) in &active {
|
||||
let ox = cx * cs;
|
||||
let oy = cy * cs;
|
||||
for ly in 0..cs {
|
||||
for lx in 0..cs {
|
||||
let wx = ox + lx;
|
||||
let wy = oy + ly;
|
||||
let cell = grid.get(wx, wy);
|
||||
if let Some(src) = material_light(cell.material) {
|
||||
let radius = src.0 as i32;
|
||||
let color = src.1;
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let tx = wx + dx;
|
||||
let ty = wy + dy;
|
||||
if !grid.in_bounds(tx, ty) {
|
||||
continue;
|
||||
}
|
||||
let dist = ((dx * dx + dy * dy) as f32).sqrt();
|
||||
if dist > radius as f32 {
|
||||
continue;
|
||||
}
|
||||
if !line_of_sight(grid, wx, wy, tx, ty) {
|
||||
continue;
|
||||
}
|
||||
let t = 1.0 - dist / radius as f32;
|
||||
let atten = t * t;
|
||||
let cur = grid.get_light(tx, ty);
|
||||
let nr = (cur[0] as f32 + color[0] as f32 * atten).min(255.0) as u8;
|
||||
let ng = (cur[1] as f32 + color[1] as f32 * atten).min(255.0) as u8;
|
||||
let nb = (cur[2] as f32 + color[2] as f32 * atten).min(255.0) as u8;
|
||||
grid.set_light(tx, ty, [nr, ng, nb]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn material_light(mat: MaterialId) -> Option<(u32, [u8; 3])> {
|
||||
match mat {
|
||||
MaterialId::Lava => Some((25, [255, 120, 30])),
|
||||
MaterialId::Fire => Some((15, [255, 180, 60])),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn line_of_sight(grid: &ChunkedGrid, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
|
||||
let dx = (x1 - x0).abs();
|
||||
let dy = (y1 - y0).abs();
|
||||
let sx = if x0 < x1 { 1 } else { -1 };
|
||||
let sy = if y0 < y1 { 1 } else { -1 };
|
||||
let mut err = dx - dy;
|
||||
let mut cx = x0;
|
||||
let mut cy = y0;
|
||||
loop {
|
||||
if cx == x1 && cy == y1 {
|
||||
return true;
|
||||
}
|
||||
if cx != x0 || cy != y0 {
|
||||
if grid.get(cx, cy).is_solid() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let e2 = 2 * err;
|
||||
if e2 > -dy {
|
||||
err -= dy;
|
||||
cx += sx;
|
||||
}
|
||||
if e2 < dx {
|
||||
err += dx;
|
||||
cy += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NEIGHBORS4: [(i32, i32); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
|
||||
|
||||
+78
-2
@@ -4,6 +4,11 @@ pub const CHUNK_SIZE: usize = 64;
|
||||
|
||||
pub struct Chunk {
|
||||
pub cells: Vec<Cell>,
|
||||
pub temps: Vec<f32>,
|
||||
pub pressure: Vec<u8>,
|
||||
pub gas_type: Vec<u8>,
|
||||
pub gas_density: Vec<u8>,
|
||||
pub light: Vec<[u8; 3]>,
|
||||
pub active: bool,
|
||||
pub modified: bool,
|
||||
pub was_modified: bool,
|
||||
@@ -11,11 +16,18 @@ pub struct Chunk {
|
||||
pub dirty: Option<(i32, i32, i32, i32)>,
|
||||
}
|
||||
|
||||
const CHUNK_AREA: usize = CHUNK_SIZE * CHUNK_SIZE;
|
||||
const ATMOSPHERIC_PRESSURE: u8 = 128;
|
||||
|
||||
impl Chunk {
|
||||
pub fn new() -> Self {
|
||||
let size = CHUNK_SIZE * CHUNK_SIZE;
|
||||
Self {
|
||||
cells: vec![Cell::empty(); size],
|
||||
cells: vec![Cell::empty(); CHUNK_AREA],
|
||||
temps: vec![20.0; CHUNK_AREA],
|
||||
pressure: vec![ATMOSPHERIC_PRESSURE; CHUNK_AREA],
|
||||
gas_type: vec![0; CHUNK_AREA],
|
||||
gas_density: vec![0; CHUNK_AREA],
|
||||
light: vec![[0, 0, 0]; CHUNK_AREA],
|
||||
active: false,
|
||||
modified: false,
|
||||
was_modified: false,
|
||||
@@ -62,10 +74,74 @@ impl Chunk {
|
||||
pub fn set_material(&mut self, x: i32, y: i32, mat: MaterialId) {
|
||||
if Self::in_bounds(x, y) {
|
||||
self.cells[Self::idx(x, y)] = Cell::new(mat);
|
||||
self.temps[Self::idx(x, y)] = crate::world::cell::default_temp(mat);
|
||||
self.modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_temp(&self, x: i32, y: i32) -> f32 {
|
||||
if !Self::in_bounds(x, y) {
|
||||
return 20.0;
|
||||
}
|
||||
self.temps[Self::idx(x, y)]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_temp(&mut self, x: i32, y: i32, t: f32) {
|
||||
if Self::in_bounds(x, y) {
|
||||
self.temps[Self::idx(x, y)] = t;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_pressure(&self, x: i32, y: i32) -> u8 {
|
||||
if !Self::in_bounds(x, y) {
|
||||
return 128;
|
||||
}
|
||||
self.pressure[Self::idx(x, y)]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_pressure(&mut self, x: i32, y: i32, p: u8) {
|
||||
if Self::in_bounds(x, y) {
|
||||
self.pressure[Self::idx(x, y)] = p;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_gas(&self, x: i32, y: i32) -> (u8, u8) {
|
||||
if !Self::in_bounds(x, y) {
|
||||
return (0, 0);
|
||||
}
|
||||
let i = Self::idx(x, y);
|
||||
(self.gas_type[i], self.gas_density[i])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_gas(&mut self, x: i32, y: i32, gas_type: u8, density: u8) {
|
||||
if Self::in_bounds(x, y) {
|
||||
let i = Self::idx(x, y);
|
||||
self.gas_type[i] = gas_type;
|
||||
self.gas_density[i] = density;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_light(&self, x: i32, y: i32) -> [u8; 3] {
|
||||
if !Self::in_bounds(x, y) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
self.light[Self::idx(x, y)]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_light(&mut self, x: i32, y: i32, rgb: [u8; 3]) {
|
||||
if Self::in_bounds(x, y) {
|
||||
self.light[Self::idx(x, y)] = rgb;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cells.iter().all(|c| c.is_empty())
|
||||
}
|
||||
|
||||
+266
-33
@@ -1,4 +1,4 @@
|
||||
use crate::world::cell::{Cell, MaterialId};
|
||||
use crate::world::cell::{default_temp, Cell, MaterialId};
|
||||
use crate::world::chunk::{Chunk, CHUNK_SIZE};
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
@@ -193,20 +193,171 @@ impl ChunkedGrid {
|
||||
return;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
let t = default_temp(mat);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
|
||||
chunk.set_material(lx, ly, mat);
|
||||
chunk.set_temp(lx, ly, t);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
chunk.set_material(lx, ly, mat);
|
||||
chunk.set_temp(lx, ly, t);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_temp(&self, x: i32, y: i32) -> f32 {
|
||||
if !self.in_bounds(x, y) {
|
||||
return 20.0;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get(idx) {
|
||||
return chunk.get_temp(lx, ly);
|
||||
}
|
||||
}
|
||||
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
|
||||
return chunk.get_temp(lx, ly);
|
||||
}
|
||||
20.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_temp(&mut self, x: i32, y: i32, t: f32) {
|
||||
if !self.in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
|
||||
chunk.set_temp(lx, ly, t);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
chunk.set_temp(lx, ly, t);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_pressure(&self, x: i32, y: i32) -> u8 {
|
||||
if !self.in_bounds(x, y) {
|
||||
return 128;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get(idx) {
|
||||
return chunk.get_pressure(lx, ly);
|
||||
}
|
||||
}
|
||||
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
|
||||
return chunk.get_pressure(lx, ly);
|
||||
}
|
||||
128
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_pressure(&mut self, x: i32, y: i32, p: u8) {
|
||||
if !self.in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
|
||||
chunk.set_pressure(lx, ly, p);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
chunk.set_pressure(lx, ly, p);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_gas(&self, x: i32, y: i32) -> (u8, u8) {
|
||||
if !self.in_bounds(x, y) {
|
||||
return (0, 0);
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get(idx) {
|
||||
return chunk.get_gas(lx, ly);
|
||||
}
|
||||
}
|
||||
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
|
||||
return chunk.get_gas(lx, ly);
|
||||
}
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_gas(&mut self, x: i32, y: i32, gas_type: u8, density: u8) {
|
||||
if !self.in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
|
||||
chunk.set_gas(lx, ly, gas_type, density);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
chunk.set_gas(lx, ly, gas_type, density);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_light(&self, x: i32, y: i32) -> [u8; 3] {
|
||||
if !self.in_bounds(x, y) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get(idx) {
|
||||
return chunk.get_light(lx, ly);
|
||||
}
|
||||
}
|
||||
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
|
||||
return chunk.get_light(lx, ly);
|
||||
}
|
||||
[0, 0, 0]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_light(&mut self, x: i32, y: i32, rgb: [u8; 3]) {
|
||||
if !self.in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let (cx, cy, lx, ly) = self.chunk_at(x, y);
|
||||
if self.is_bounded() {
|
||||
if let Some(idx) = self.chunk_index(cx, cy) {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
|
||||
chunk.set_light(lx, ly, rgb);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
chunk.set_light(lx, ly, rgb);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mark_dirty(&mut self, x: i32, y: i32) {
|
||||
if !self.in_bounds(x, y) {
|
||||
@@ -284,6 +435,12 @@ impl ChunkedGrid {
|
||||
}
|
||||
let (cx1, cy1, lx1, ly1) = self.chunk_at(x1, y1);
|
||||
let (cx2, cy2, lx2, ly2) = self.chunk_at(x2, y2);
|
||||
let t1 = self.get_temp(x1, y1);
|
||||
let t2 = self.get_temp(x2, y2);
|
||||
let p1 = self.get_pressure(x1, y1);
|
||||
let p2 = self.get_pressure(x2, y2);
|
||||
let g1 = self.get_gas(x1, y1);
|
||||
let g2 = self.get_gas(x2, y2);
|
||||
if self.is_bounded() {
|
||||
let idx1 = self.chunk_index(cx1, cy1);
|
||||
let idx2 = self.chunk_index(cx2, cy2);
|
||||
@@ -292,9 +449,11 @@ impl ChunkedGrid {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(i1) {
|
||||
let ci1 = (ly1 as usize) * self.chunk_size + (lx1 as usize);
|
||||
let ci2 = (ly2 as usize) * self.chunk_size + (lx2 as usize);
|
||||
let tmp = chunk.cells[ci1];
|
||||
chunk.cells[ci1] = chunk.cells[ci2];
|
||||
chunk.cells[ci2] = tmp;
|
||||
chunk.cells.swap(ci1, ci2);
|
||||
chunk.temps.swap(ci1, ci2);
|
||||
chunk.pressure.swap(ci1, ci2);
|
||||
chunk.gas_type.swap(ci1, ci2);
|
||||
chunk.gas_density.swap(ci1, ci2);
|
||||
chunk.cells[ci2].updated_this_tick = true;
|
||||
chunk.modified = true;
|
||||
chunk.mark_dirty(lx1, ly1);
|
||||
@@ -307,6 +466,10 @@ impl ChunkedGrid {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(i1) {
|
||||
let ci = (ly1 as usize) * self.chunk_size + (lx1 as usize);
|
||||
chunk.cells[ci] = c2;
|
||||
chunk.temps[ci] = t2;
|
||||
chunk.pressure[ci] = p2;
|
||||
chunk.gas_type[ci] = g2.0;
|
||||
chunk.gas_density[ci] = g2.1;
|
||||
chunk.cells[ci].updated_this_tick = true;
|
||||
chunk.modified = true;
|
||||
chunk.mark_dirty(lx1, ly1);
|
||||
@@ -314,6 +477,10 @@ impl ChunkedGrid {
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(i2) {
|
||||
let ci = (ly2 as usize) * self.chunk_size + (lx2 as usize);
|
||||
chunk.cells[ci] = c1;
|
||||
chunk.temps[ci] = t1;
|
||||
chunk.pressure[ci] = p1;
|
||||
chunk.gas_type[ci] = g1.0;
|
||||
chunk.gas_density[ci] = g1.1;
|
||||
chunk.modified = true;
|
||||
chunk.mark_dirty(lx2, ly2);
|
||||
}
|
||||
@@ -325,9 +492,11 @@ impl ChunkedGrid {
|
||||
let chunk = self.get_or_create_chunk(cx1, cy1);
|
||||
let i1 = (ly1 as usize) * cs + (lx1 as usize);
|
||||
let i2 = (ly2 as usize) * cs + (lx2 as usize);
|
||||
let tmp = chunk.cells[i1];
|
||||
chunk.cells[i1] = chunk.cells[i2];
|
||||
chunk.cells[i2] = tmp;
|
||||
chunk.cells.swap(i1, i2);
|
||||
chunk.temps.swap(i1, i2);
|
||||
chunk.pressure.swap(i1, i2);
|
||||
chunk.gas_type.swap(i1, i2);
|
||||
chunk.gas_density.swap(i1, i2);
|
||||
chunk.cells[i2].updated_this_tick = true;
|
||||
chunk.modified = true;
|
||||
chunk.mark_dirty(lx1, ly1);
|
||||
@@ -336,7 +505,13 @@ impl ChunkedGrid {
|
||||
let c1 = self.get(x1, y1);
|
||||
let c2 = self.get(x2, y2);
|
||||
self.set(x1, y1, c2);
|
||||
self.set_temp(x1, y1, t2);
|
||||
self.set_pressure(x1, y1, p2);
|
||||
self.set_gas(x1, y1, g2.0, g2.1);
|
||||
self.set(x2, y2, c1);
|
||||
self.set_temp(x2, y2, t1);
|
||||
self.set_pressure(x2, y2, p1);
|
||||
self.set_gas(x2, y2, g1.0, g1.1);
|
||||
let cs = self.chunk_size;
|
||||
let chunk = self.get_or_create_chunk(cx1, cy1);
|
||||
let i = (ly1 as usize) * cs + (lx1 as usize);
|
||||
@@ -653,17 +828,25 @@ impl ChunkedGrid {
|
||||
None => return Ok(()),
|
||||
}
|
||||
};
|
||||
let (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
|
||||
let w = (x1 - x0) as usize;
|
||||
let h = (y1 - y0) as usize;
|
||||
let mut bytes = Vec::with_capacity(w * h * 12);
|
||||
let cs = self.chunk_size as i32;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let lx = x - cx * cs;
|
||||
let ly = y - cy * cs;
|
||||
bytes.extend_from_slice(&chunk.get(lx, ly).to_bytes());
|
||||
}
|
||||
let area = self.chunk_size * self.chunk_size;
|
||||
let mut bytes = Vec::with_capacity(5 + area * 8 + area * 4 + area * 2 + area + area * 3);
|
||||
bytes.extend_from_slice(b"VWM1");
|
||||
bytes.push(1);
|
||||
for c in &chunk.cells {
|
||||
bytes.extend_from_slice(&c.to_bytes());
|
||||
}
|
||||
for t in &chunk.temps {
|
||||
bytes.extend_from_slice(&t.to_le_bytes());
|
||||
}
|
||||
for i in 0..area {
|
||||
bytes.push(chunk.gas_type[i]);
|
||||
bytes.push(chunk.gas_density[i]);
|
||||
}
|
||||
for &p in &chunk.pressure {
|
||||
bytes.push(p);
|
||||
}
|
||||
for l in &chunk.light {
|
||||
bytes.extend_from_slice(&l[..]);
|
||||
}
|
||||
let dir = Path::new(path);
|
||||
if let Some(parent) = dir.parent() {
|
||||
@@ -678,23 +861,72 @@ impl ChunkedGrid {
|
||||
|
||||
fn load_chunk_from_path(&mut self, path: &Path, cx: i32, cy: i32) -> io::Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
let (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
|
||||
let w = (x1 - x0) as usize;
|
||||
let h = (y1 - y0) as usize;
|
||||
let expected = w * h * 12;
|
||||
if data.len() != expected {
|
||||
return Err(io::Error::other("chunk file size mismatch"));
|
||||
}
|
||||
let cs = self.chunk_size as i32;
|
||||
let cs = self.chunk_size;
|
||||
let area = cs * cs;
|
||||
let bounds = self.chunk_bounds(cx, cy);
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
let mut i = 0usize;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let lx = x - cx * cs;
|
||||
let ly = y - cy * cs;
|
||||
let cell = Cell::from_bytes(&data[i * 12..(i + 1) * 12]);
|
||||
if data.len() >= 5 && &data[0..4] == b"VWM1" {
|
||||
let mut off = 5;
|
||||
for i in 0..area {
|
||||
if off + 8 > data.len() {
|
||||
break;
|
||||
}
|
||||
let cell = Cell::from_bytes(&data[off..off + 8]);
|
||||
let lx = (i % cs) as i32;
|
||||
let ly = (i / cs) as i32;
|
||||
chunk.set(lx, ly, cell);
|
||||
i += 1;
|
||||
off += 8;
|
||||
}
|
||||
for i in 0..area {
|
||||
if off + 4 > data.len() {
|
||||
break;
|
||||
}
|
||||
chunk.temps[i] =
|
||||
f32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
|
||||
off += 4;
|
||||
}
|
||||
for i in 0..area {
|
||||
if off + 2 > data.len() {
|
||||
break;
|
||||
}
|
||||
chunk.gas_type[i] = data[off];
|
||||
chunk.gas_density[i] = data[off + 1];
|
||||
off += 2;
|
||||
}
|
||||
for i in 0..area {
|
||||
if off >= data.len() {
|
||||
break;
|
||||
}
|
||||
chunk.pressure[i] = data[off];
|
||||
off += 1;
|
||||
}
|
||||
for i in 0..area {
|
||||
if off + 3 > data.len() {
|
||||
break;
|
||||
}
|
||||
chunk.light[i] = [data[off], data[off + 1], data[off + 2]];
|
||||
off += 3;
|
||||
}
|
||||
} else {
|
||||
let (x0, y0, x1, y1) = bounds;
|
||||
let w = (x1 - x0) as usize;
|
||||
let h = (y1 - y0) as usize;
|
||||
let expected = w * h * 12;
|
||||
if data.len() != expected {
|
||||
return Err(io::Error::other("chunk file size mismatch"));
|
||||
}
|
||||
let mut i = 0usize;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let lx = x - cx * cs as i32;
|
||||
let ly = y - cy * cs as i32;
|
||||
let cell = Cell::from_bytes(&data[i * 12..(i + 1) * 12]);
|
||||
chunk.set(lx, ly, cell);
|
||||
if cell.material != MaterialId::Empty {
|
||||
chunk.set_temp(lx, ly, default_temp(cell.material));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
chunk.active = true;
|
||||
@@ -716,6 +948,7 @@ impl ChunkedGrid {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if let Some(rest) = name.strip_prefix("chunk_") {
|
||||
let rest = rest.strip_suffix(".bin").unwrap_or(rest);
|
||||
let parts: Vec<&str> = rest.split('_').collect();
|
||||
if parts.len() == 2 {
|
||||
if let (Ok(cx), Ok(cy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
|
||||
|
||||
+3
-3
@@ -321,7 +321,7 @@ impl Grid {
|
||||
let (x0, y0, x1, y1) = self.chunk_bounds(cx as usize, cy as usize);
|
||||
let w = (x1 - x0) as usize;
|
||||
let h = (y1 - y0) as usize;
|
||||
let mut bytes = Vec::with_capacity(w * h * 12);
|
||||
let mut bytes = Vec::with_capacity(w * h * 8);
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
bytes.extend_from_slice(&self.get(x, y).to_bytes());
|
||||
@@ -342,14 +342,14 @@ impl Grid {
|
||||
let (x0, y0, x1, y1) = self.chunk_bounds(cx as usize, cy as usize);
|
||||
let w = (x1 - x0) as usize;
|
||||
let h = (y1 - y0) as usize;
|
||||
let expected = w * h * 12;
|
||||
let expected = w * h * 8;
|
||||
if data.len() != expected {
|
||||
return Err(io::Error::other("chunk file size mismatch"));
|
||||
}
|
||||
let mut i = 0usize;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let cell = Cell::from_bytes(&data[i * 12..(i + 1) * 12]);
|
||||
let cell = Cell::from_bytes(&data[i * 8..(i + 1) * 8]);
|
||||
self.set(x, y, cell);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user