Files
Verbatim/src/world/worldgen.rs
T
Emil 22de8bc42e fix: wider surface terrain — lower noise frequencies for rolling hills
base freq 0.08→0.012 (~260 cell wide hills, was ~40)
detail freq 0.23→0.04 (~78 cell features, was ~14)
micro freq 0.57→0.11 (~28 cell ripples, was ~6)
amplitudes reduced: base ±20→±15, detail ±10→±7.5, micro ±5→±2.5

Result: wide rolling hills instead of narrow sharp peaks.
2026-06-22 17:05:38 +03:00

1296 lines
41 KiB
Rust

use crate::entity::player::Player;
use crate::entity::{EntityManager, ItemManager, ItemType};
use crate::world::cell::{Cell, MaterialId};
use crate::world::cellular::CellularAutomaton;
use crate::world::chunk::CHUNK_SIZE;
use crate::world::chunked_grid::ChunkedGrid;
pub const WORLD_SCALE: i32 = 5;
#[inline]
fn sc(v: i32) -> i32 {
v * WORLD_SCALE
}
#[inline]
fn scf(v: f32) -> f32 {
v * WORLD_SCALE as f32
}
pub struct WorldGenerator<'a> {
ca: &'a mut CellularAutomaton,
}
#[derive(Clone, Copy, Debug)]
struct Rect {
x: i32,
y: i32,
w: i32,
h: i32,
}
impl<'a> WorldGenerator<'a> {
pub fn new(ca: &'a mut CellularAutomaton) -> Self {
Self { ca }
}
pub fn generate(
&mut self,
grid: &mut ChunkedGrid,
items: &mut ItemManager,
player: &mut Player,
entities: &mut EntityManager,
depth: u32,
) -> (f32, f32) {
let (px, py) = if grid.width <= 2048 && grid.height <= 2048 {
if depth <= 3 {
self.generate_surface(grid, depth)
} else if depth <= 6 {
self.generate_caves(grid, depth)
} else {
self.generate_dungeon(grid, items, depth)
}
} else {
self.generate_spawn_region(grid, items, depth)
};
player.spawn_at(entities, px as f32, py as f32);
if let Some(e) = player.entity_mut(entities) {
e.facing_right = true;
}
self.place_items(grid, items, px, py, depth);
(px as f32, py as f32)
}
fn generate_spawn_region(
&mut self,
grid: &mut ChunkedGrid,
_items: &mut ItemManager,
_depth: u32,
) -> (i32, i32) {
let sx = if grid.is_infinite() {
1000
} else {
(grid.width as i32 / 2).max(100)
};
let surface_y = self.surface_height_world(sx);
let spawn_cx = sx / CHUNK_SIZE as i32;
let spawn_cy = (surface_y / CHUNK_SIZE as i32).max(0);
for dy in -1..=2 {
for dx in -2..=2 {
let cx = spawn_cx + dx;
let cy = spawn_cy + dy;
if grid.in_bounds(cx * CHUNK_SIZE as i32, cy * CHUNK_SIZE as i32) {
self.generate_chunk(grid, cx, cy);
}
}
}
grid.set_material(sx, surface_y, MaterialId::Stairs);
if grid.get(sx, surface_y + 1).is_empty() {
grid.set_material(sx, surface_y + 1, MaterialId::Stone);
}
let spawn_y = surface_y - sc(8);
(sx, spawn_y)
}
pub fn generate_chunk(&mut self, grid: &mut ChunkedGrid, cx: i32, cy: i32) {
if grid.is_chunk_generated(cx, cy) {
return;
}
if cy < 2 {
self.generate_surface_chunk(grid, cx, cy);
} else if cy < 6 {
self.generate_cave_chunk(grid, cx, cy);
} else {
self.generate_dungeon_chunk(grid, cx, cy);
}
if let Some(chunk) = grid.get_chunk_mut(cx, cy) {
chunk.generated = true;
chunk.modified = true;
chunk.was_modified = false;
chunk.dirty = None;
}
}
fn generate_surface_chunk(&mut self, grid: &mut ChunkedGrid, cx: i32, cy: i32) {
let x0 = cx * CHUNK_SIZE as i32;
let y0 = cy * CHUNK_SIZE as i32;
let mut surface = Vec::with_capacity(CHUNK_SIZE);
for lx in 0..CHUNK_SIZE as i32 {
let x = x0 + lx;
let s = self.surface_height_world(x);
surface.push(s);
for y in y0..(y0 + CHUNK_SIZE as i32) {
if y < s {
continue;
}
if y == s {
grid.set_material(x, y, MaterialId::Grass);
} else if y > s + sc(8) {
grid.set_material(x, y, MaterialId::Stone);
} else {
grid.set_material(x, y, MaterialId::Dirt);
}
}
}
let chunk_depth = (cy / 2).max(1) as u32;
self.place_surface_features_chunk(grid, cx, cy, &surface, chunk_depth);
grid.fill_border(MaterialId::Stone);
}
fn generate_cave_chunk(&mut self, grid: &mut ChunkedGrid, cx: i32, cy: i32) {
let x0 = cx * CHUNK_SIZE as i32;
let y0 = cy * CHUNK_SIZE as i32;
let depth = (cy / 2).max(2) as u32;
for y in y0..(y0 + CHUNK_SIZE as i32) {
for x in x0..(x0 + CHUNK_SIZE as i32) {
grid.set_material(x, y, MaterialId::Stone);
}
}
let fill_prob = 0.42;
let iterations = 4;
self.carve_ca_caves_chunk(grid, cx, cy, fill_prob, iterations);
let pool_count = (1 + depth / 3).min(3) as i32;
for _ in 0..pool_count {
let typ = self.random_cave_pool_type(depth);
self.place_cave_pool_chunk(grid, cx, cy, typ, sc(2), sc(4));
}
grid.fill_border(MaterialId::Stone);
}
fn generate_dungeon_chunk(&mut self, grid: &mut ChunkedGrid, cx: i32, cy: i32) {
let x0 = cx * CHUNK_SIZE as i32;
let y0 = cy * CHUNK_SIZE as i32;
for y in y0..(y0 + CHUNK_SIZE as i32) {
for x in x0..(x0 + CHUNK_SIZE as i32) {
grid.set_material(x, y, MaterialId::Stone);
}
}
let root = Rect {
x: x0 + 6,
y: y0 + 6,
w: CHUNK_SIZE as i32 - 12,
h: CHUNK_SIZE as i32 - 12,
};
let tree = self.build_bsp(root);
let mut rooms = Vec::new();
self.collect_rooms(&tree, &mut rooms);
for room in &rooms {
self.carve_room(grid, *room);
}
self.connect_bsp_rooms(&tree, grid);
grid.fill_border(MaterialId::Stone);
if let Some(chunk) = grid.get_chunk_mut(cx, cy) {
chunk.modified = true;
chunk.was_modified = true;
}
}
fn generate_surface(&mut self, grid: &mut ChunkedGrid, depth: u32) -> (i32, i32) {
let w = grid.width as i32;
let h = grid.height as i32;
let mut surface = vec![h - 3; grid.width];
for x in 0..grid.width {
let s = self.surface_height(x as i32, h);
surface[x] = s;
for y in s..h - 2 {
if y == s {
let mat = if depth == 1 {
MaterialId::Grass
} else {
MaterialId::Dirt
};
grid.set_material(x as i32, y, mat);
} else if y > s + sc(8) {
grid.set_material(x as i32, y, MaterialId::Stone);
} else {
grid.set_material(x as i32, y, MaterialId::Dirt);
}
}
grid.set_material(x as i32, h - 2, MaterialId::Dirt);
grid.set_material(x as i32, h - 1, MaterialId::Stone);
}
self.carve_underground_caves(grid, &surface);
self.place_surface_features(grid, &surface, depth);
grid.fill_border(MaterialId::Stone);
for cy in 0..grid.chunks_y as i32 {
for cx in 0..grid.chunks_x as i32 {
grid.set_chunk_dirty(cx, cy, None);
if let Some(chunk) = grid.get_chunk_mut(cx, cy) {
chunk.modified = false;
chunk.was_modified = false;
}
}
}
let sx = w / 2;
let mut surface_y = h - 3;
for y in 0..h {
let cell = grid.get(sx, y);
if cell.is_solid() && cell.material != MaterialId::Stone {
surface_y = y;
break;
}
}
grid.set_material(sx, surface_y, MaterialId::Stairs);
if grid.get(sx, surface_y + 1).is_empty() {
grid.set_material(sx, surface_y + 1, MaterialId::Stone);
}
let spawn_y = surface_y - 5;
(sx, spawn_y)
}
fn generate_caves(&mut self, grid: &mut ChunkedGrid, depth: u32) -> (i32, i32) {
let w = grid.width as i32;
let h = grid.height as i32;
for y in 0..h {
for x in 0..w {
grid.set_material(x, y, MaterialId::Stone);
}
}
self.carve_ca_caves(grid, 0.38, 5);
self.seal_disconnected_regions(grid);
let pool_count = (2 + depth / 2).min(6) as i32;
for _ in 0..pool_count {
let typ = self.random_cave_pool_type(depth);
self.place_cave_pool(grid, typ, sc(3), sc(5));
}
let stalactites = self.random_range_i32(3, 7);
for _ in 0..stalactites {
let x = self.random_range_i32(8, w - 8);
let y_top = self.random_range_i32(3, 12);
let len = self.random_range_i32(sc(3), sc(9));
for dy in 0..len {
let y = y_top + dy;
if grid.get(x, y).material != MaterialId::Stone {
continue;
}
grid.set_material(x, y, MaterialId::Stone);
}
}
grid.fill_border(MaterialId::Stone);
for cy in 0..grid.chunks_y as i32 {
for cx in 0..grid.chunks_x as i32 {
grid.set_chunk_dirty(cx, cy, None);
if let Some(chunk) = grid.get_chunk_mut(cx, cy) {
chunk.modified = false;
chunk.was_modified = false;
}
}
}
let spawn = self.find_safe_spawn(grid);
let stairs = self.find_empty_with_floor(grid, w - 15, h - 15);
if let Some((sx, sy)) = stairs {
grid.set_material(sx, sy, MaterialId::Stairs);
if grid.get(sx, sy + 1).is_empty() {
grid.set_material(sx, sy + 1, MaterialId::Stone);
}
}
spawn
}
fn generate_dungeon(
&mut self,
grid: &mut ChunkedGrid,
items: &mut ItemManager,
_depth: u32,
) -> (i32, i32) {
let w = grid.width as i32;
let h = grid.height as i32;
for y in 0..h {
for x in 0..w {
grid.set_material(x, y, MaterialId::Stone);
}
}
let root = Rect {
x: 8,
y: 8,
w: w - 16,
h: h - 16,
};
let tree = self.build_bsp(root);
let mut rooms = Vec::new();
self.collect_rooms(&tree, &mut rooms);
if rooms.is_empty() {
return self.fallback_spawn(grid);
}
for room in &rooms {
self.carve_room(grid, *room);
}
self.connect_bsp_rooms(&tree, grid);
grid.fill_border(MaterialId::Stone);
for cy in 0..grid.chunks_y as i32 {
for cx in 0..grid.chunks_x as i32 {
grid.set_chunk_dirty(cx, cy, None);
if let Some(chunk) = grid.get_chunk_mut(cx, cy) {
chunk.modified = false;
chunk.was_modified = false;
}
}
}
let spawn_room = rooms[0];
let spawn_x = spawn_room.x + spawn_room.w / 2;
let spawn_y = spawn_room.y + spawn_room.h - 5;
let spawn = (spawn_x, spawn_y);
let mut stairs_room = rooms[0];
let mut best_dist = 0;
for room in &rooms {
let cx = room.x + room.w / 2;
let cy = room.y + room.h / 2;
let d = (cx - spawn_x).abs() + (cy - spawn_y).abs();
if d > best_dist {
best_dist = d;
stairs_room = *room;
}
}
let sx = stairs_room.x + stairs_room.w / 2;
let sy = stairs_room.y + stairs_room.h;
grid.set_material(sx, sy, MaterialId::Stairs);
let item_count = (3 + self.random_range_i32(0, 3)).min(rooms.len() as i32);
let mut placed_rooms = rooms.clone();
self.shuffle_rooms(&mut placed_rooms);
for i in 0..item_count as usize {
let room = placed_rooms[i];
let (ix, iy) = self.random_point_in_room(room);
if let Some(typ) = self.random_item_type() {
items.spawn(typ, ix, iy);
}
}
let trap_count = self.random_range_i32(1, 3).min(rooms.len() as i32);
for _ in 0..trap_count as usize {
let room = rooms[self.random_usize(rooms.len())];
let (tx, ty) = self.random_point_in_room(room);
if grid.get(tx, ty).is_empty() {
grid.set_material(tx, ty, MaterialId::Acid);
}
}
spawn
}
fn place_items(
&mut self,
grid: &ChunkedGrid,
items: &mut ItemManager,
px: i32,
py: i32,
depth: u32,
) {
if depth <= 3 {
let base_y = py + 1;
let offsets = [
(sc(-6), sc(1)),
(sc(6), sc(1)),
(sc(-3), sc(-8)),
(sc(10), sc(1)),
(sc(-10), sc(1)),
(sc(3), sc(-6)),
];
let types = [
ItemType::Sword,
ItemType::HealthPotion,
ItemType::LeatherArmor,
ItemType::Bow,
ItemType::Shield,
ItemType::ManaPotion,
];
for (i, (dx, dy)) in offsets.iter().enumerate() {
let x = px + dx;
let y = base_y + dy;
if grid.in_bounds(x, y) {
items.spawn(types[i], x, y);
}
}
}
}
fn place_surface_features(&mut self, grid: &mut ChunkedGrid, surface: &[i32], depth: u32) {
let tree_count = self.random_range_i32(3, 7);
self.place_trees(grid, surface, tree_count);
let pool_count = (2 + depth / 2).min(5) as i32;
for _ in 0..pool_count {
let typ = self.random_surface_pool_type(depth);
self.place_surface_pool(grid, typ, sc(3), sc(6));
}
let dune_count = self.random_range_i32(1, 4);
self.place_sand_dunes(grid, surface, dune_count);
let wall_count = self.random_range_i32(1, 4);
self.place_walls(grid, surface, wall_count);
}
fn place_surface_features_chunk(
&mut self,
grid: &mut ChunkedGrid,
cx: i32,
_cy: i32,
surface: &[i32],
depth: u32,
) {
let x0 = cx * CHUNK_SIZE as i32;
let x1 = x0 + CHUNK_SIZE as i32;
let tree_count = self.random_range_i32(0, 2);
self.place_trees_chunk(grid, x0, x1, surface, tree_count);
let pool_count = (depth / 2).min(2) as i32;
for _ in 0..pool_count {
let typ = self.random_surface_pool_type(depth);
self.place_surface_pool_chunk(grid, x0, x1, surface, typ, sc(2), sc(4));
}
let dune_count = self.random_range_i32(0, 2);
self.place_sand_dunes_chunk(grid, x0, x1, surface, dune_count);
let wall_count = self.random_range_i32(0, 2);
self.place_walls_chunk(grid, x0, x1, surface, wall_count);
}
fn place_trees_chunk(
&mut self,
grid: &mut ChunkedGrid,
x0: i32,
_x1: i32,
surface: &[i32],
count: i32,
) {
for _ in 0..count {
let lx = self.random_range_i32(2, CHUNK_SIZE as i32 - 2);
let x = x0 + lx;
let s = surface[lx as usize];
if s < 10 {
continue;
}
for y in (s - sc(6))..s {
if grid.in_bounds(x, y) {
grid.set_material(x, y, MaterialId::Wood);
}
}
for dy in -sc(2)..=0 {
for dx in -sc(2)..=sc(2) {
if dx * dx + dy * dy <= sc(5) {
let cx = x + dx;
let cy = s - sc(6) + dy;
if grid.in_bounds(cx, cy) && grid.get(cx, cy).is_empty() {
grid.set_material(cx, cy, MaterialId::Grass);
}
}
}
}
}
}
fn place_surface_pool_chunk(
&mut self,
grid: &mut ChunkedGrid,
x0: i32,
_x1: i32,
surface: &[i32],
typ: MaterialId,
min_r: i32,
max_r: i32,
) {
let lx = self.random_range_i32(4, CHUNK_SIZE as i32 - 4);
let x = x0 + lx;
let s = surface[lx as usize];
let r = self.random_range_i32(min_r, max_r);
let cy = self.random_range_i32(s + 2, s + r + 2);
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r {
let px = x + dx;
let py = cy + dy;
if grid.in_bounds(px, py) {
grid.set_material(px, py, typ);
}
}
}
}
}
fn place_cave_pool_chunk(
&mut self,
grid: &mut ChunkedGrid,
cx: i32,
cy: i32,
typ: MaterialId,
min_r: i32,
max_r: i32,
) {
let x0 = cx * CHUNK_SIZE as i32;
let y0 = cy * CHUNK_SIZE as i32;
let x1 = x0 + CHUNK_SIZE as i32;
let y1 = y0 + CHUNK_SIZE as i32;
for _ in 0..100 {
let px = self.random_range_i32(x0 + 4, x1 - 4);
let py = self.random_range_i32(y0 + 4, y1 - 4);
if grid.get(px, py).is_empty() {
continue;
}
let mut has_empty_neighbor = false;
for (dx, dy) in NEIGHBORS4 {
if grid.get(px + dx, py + dy).is_empty() {
has_empty_neighbor = true;
break;
}
}
if !has_empty_neighbor {
continue;
}
let r = self.random_range_i32(min_r, max_r);
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r {
let x = px + dx;
let y = py + dy;
if grid.in_bounds(x, y) && !grid.get(x, y).is_empty() {
grid.set_material(x, y, typ);
}
}
}
}
return;
}
}
fn place_sand_dunes_chunk(
&mut self,
grid: &mut ChunkedGrid,
x0: i32,
x1: i32,
surface: &[i32],
count: i32,
) {
for _ in 0..count {
let lx = self.random_range_i32(4, (x1 - x0 - 4).max(5));
let x = x0 + lx;
let s = surface[lx as usize];
let width = self.random_range_i32(sc(4), sc(10));
for dx in -width / 2..=width / 2 {
let pile = (width / 2 - dx.abs()).max(1) + 1;
for dy in 0..pile {
let y = s - 1 - dy;
if grid.in_bounds(x + dx, y) && grid.get(x + dx, y).is_empty() {
grid.set_material(x + dx, y, MaterialId::Sand);
}
}
}
}
}
fn place_walls_chunk(
&mut self,
grid: &mut ChunkedGrid,
x0: i32,
x1: i32,
surface: &[i32],
count: i32,
) {
for _ in 0..count {
let lx = self.random_range_i32(4, (x1 - x0 - 4).max(5));
let x = x0 + lx;
let s = surface[lx as usize];
let h = self.random_range_i32(sc(2), sc(5));
for y in (s - h)..s {
if grid.in_bounds(x, y) {
grid.set_material(x, y, MaterialId::Stone);
}
if grid.in_bounds(x + 1, y) {
grid.set_material(x + 1, y, MaterialId::Stone);
}
if grid.in_bounds(x + 2, y) {
grid.set_material(x + 2, y, MaterialId::Stone);
}
if grid.in_bounds(x + 3, y) {
grid.set_material(x + 3, y, MaterialId::Stone);
}
if grid.in_bounds(x + 4, y) {
grid.set_material(x + 4, y, MaterialId::Stone);
}
}
}
}
fn place_trees(&mut self, grid: &mut ChunkedGrid, surface: &[i32], count: i32) {
let w = grid.width as i32;
for _ in 0..count {
let x = self.random_range_i32(10, w - 10);
let s = surface[x as usize];
if s < 10 {
continue;
}
for y in (s - sc(6))..s {
if grid.in_bounds(x, y) {
grid.set_material(x, y, MaterialId::Wood);
}
}
for dy in -sc(2)..=0 {
for dx in -sc(2)..=sc(2) {
if dx * dx + dy * dy <= sc(5) {
let cx = x + dx;
let cy = s - sc(6) + dy;
if grid.in_bounds(cx, cy) && grid.get(cx, cy).is_empty() {
grid.set_material(cx, cy, MaterialId::Grass);
}
}
}
}
}
}
fn place_surface_pool(
&mut self,
grid: &mut ChunkedGrid,
typ: MaterialId,
min_r: i32,
max_r: i32,
) {
let w = grid.width as i32;
let cx = self.random_range_i32(15, w - 15);
let surface = self.find_surface_near(grid, cx);
let r = self.random_range_i32(min_r, max_r);
let cy = self.random_range_i32(surface + 2, surface + r + 2);
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r {
let x = cx + dx;
let y = cy + dy;
if grid.in_bounds(x, y) {
grid.set_material(x, y, typ);
}
}
}
}
}
fn place_cave_pool(&mut self, grid: &mut ChunkedGrid, typ: MaterialId, min_r: i32, max_r: i32) {
let w = grid.width as i32;
let h = grid.height as i32;
for _ in 0..100 {
let cx = self.random_range_i32(10, w - 10);
let cy = self.random_range_i32(10, h - 10);
if grid.get(cx, cy).is_empty() {
continue;
}
let mut has_empty_neighbor = false;
for (dx, dy) in NEIGHBORS4 {
if grid.get(cx + dx, cy + dy).is_empty() {
has_empty_neighbor = true;
break;
}
}
if !has_empty_neighbor {
continue;
}
let r = self.random_range_i32(min_r, max_r);
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r {
let x = cx + dx;
let y = cy + dy;
if grid.in_bounds(x, y) && !grid.get(x, y).is_empty() {
grid.set_material(x, y, typ);
}
}
}
}
return;
}
}
fn place_sand_dunes(&mut self, grid: &mut ChunkedGrid, surface: &[i32], count: i32) {
let w = grid.width as i32;
for _ in 0..count {
let x = self.random_range_i32(15, w - 15);
let s = surface[x as usize];
let width = self.random_range_i32(sc(6), sc(16));
for dx in -width / 2..=width / 2 {
let pile = (width / 2 - dx.abs()).max(1) + 1;
for dy in 0..pile {
let y = s - 1 - dy;
if grid.in_bounds(x + dx, y) && grid.get(x + dx, y).is_empty() {
grid.set_material(x + dx, y, MaterialId::Sand);
}
}
}
}
}
fn place_walls(&mut self, grid: &mut ChunkedGrid, surface: &[i32], count: i32) {
let w = grid.width as i32;
for _ in 0..count {
let x = self.random_range_i32(10, w - 10);
let s = surface[x as usize];
let h = self.random_range_i32(sc(3), sc(7));
for y in (s - h)..s {
if grid.in_bounds(x, y) {
grid.set_material(x, y, MaterialId::Stone);
}
if grid.in_bounds(x + 1, y) {
grid.set_material(x + 1, y, MaterialId::Stone);
}
if grid.in_bounds(x + 2, y) {
grid.set_material(x + 2, y, MaterialId::Stone);
}
if grid.in_bounds(x + 3, y) {
grid.set_material(x + 3, y, MaterialId::Stone);
}
if grid.in_bounds(x + 4, y) {
grid.set_material(x + 4, y, MaterialId::Stone);
}
}
}
}
fn carve_underground_caves(&mut self, grid: &mut ChunkedGrid, surface: &[i32]) {
let w = grid.width as i32;
let h = grid.height as i32;
for _ in 0..8 {
let cx = self.random_range_i32(10, w - 10);
let lower = (h * 2 / 3).max(surface[cx as usize] + 12);
let cy = self.random_range_i32(lower, h - 10);
let r = self.random_range_i32(sc(3), sc(6));
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r {
let x = cx + dx;
let y = cy + dy;
if grid.in_bounds(x, y) && y > surface[x as usize] + 4 {
grid.set(x, y, Cell::empty());
}
}
}
}
}
}
fn carve_ca_caves(&mut self, grid: &mut ChunkedGrid, fill_prob: f64, iterations: i32) {
let w = grid.width as i32;
let h = grid.height as i32;
for y in 1..h - 1 {
for x in 1..w - 1 {
if self.ca.random_u32() as f64 / (u32::MAX as f64) < fill_prob {
grid.set(x, y, Cell::empty());
}
}
}
let size = (w * h) as usize;
let mut buf = Vec::with_capacity(size);
for y in 0..h {
for x in 0..w {
buf.push(grid.get(x, y));
}
}
for _ in 0..iterations {
for y in 1..h - 1 {
for x in 1..w - 1 {
let walls = self.wall_count(grid, x, y);
let i = (y * w + x) as usize;
if walls > 4 {
buf[i] = Cell::new(MaterialId::Stone);
} else if walls < 4 {
buf[i] = Cell::empty();
}
}
}
for y in 0..h {
for x in 0..w {
let i = (y * w + x) as usize;
grid.set(x, y, buf[i]);
}
}
}
}
fn carve_ca_caves_chunk(
&mut self,
grid: &mut ChunkedGrid,
cx: i32,
cy: i32,
fill_prob: f64,
iterations: i32,
) {
let x0 = cx * CHUNK_SIZE as i32;
let y0 = cy * CHUNK_SIZE as i32;
let x1 = x0 + CHUNK_SIZE as i32;
let y1 = y0 + CHUNK_SIZE as i32;
for y in y0 + 1..y1 - 1 {
for x in x0 + 1..x1 - 1 {
if self.ca.random_u32() as f64 / (u32::MAX as f64) < fill_prob {
grid.set(x, y, Cell::empty());
}
}
}
let size = CHUNK_SIZE * CHUNK_SIZE;
let mut buf = Vec::with_capacity(size);
for y in y0..y1 {
for x in x0..x1 {
buf.push(grid.get(x, y));
}
}
let w = CHUNK_SIZE as i32;
for _ in 0..iterations {
for y in y0 + 1..y1 - 1 {
for x in x0 + 1..x1 - 1 {
let walls = self.wall_count(grid, x, y);
let ly = y - y0;
let lx = x - x0;
let i = (ly * w + lx) as usize;
if walls > 4 {
buf[i] = Cell::new(MaterialId::Stone);
} else if walls < 4 {
buf[i] = Cell::empty();
}
}
}
for y in y0..y1 {
for x in x0..x1 {
let ly = y - y0;
let lx = x - x0;
let i = (ly * w + lx) as usize;
grid.set(x, y, buf[i]);
}
}
}
}
fn wall_count(&self, grid: &ChunkedGrid, x: i32, y: i32) -> i32 {
let mut count = 0;
for dy in -1..=1 {
for dx in -1..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = x + dx;
let ny = y + dy;
if !grid.in_bounds(nx, ny) {
count += 1;
} else {
let cell = grid.get(nx, ny);
if cell.is_solid() || cell.material == MaterialId::Stone {
count += 1;
}
}
}
}
count
}
fn seal_disconnected_regions(&mut self, grid: &mut ChunkedGrid) {
let w = grid.width as i32;
let h = grid.height as i32;
let size = (w * h) as usize;
let mut visited = vec![false; size];
let mut best = Vec::new();
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) as usize;
if grid.get(x, y).is_empty() && !visited[idx] {
let mut comp = Vec::new();
self.flood_fill(grid, &mut visited, x, y, &mut comp);
if comp.len() > best.len() {
best = comp;
}
}
}
}
let mut in_best = vec![false; size];
for (x, y) in &best {
in_best[(*y * w + *x) as usize] = true;
}
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) as usize;
if grid.get(x, y).is_empty() && !in_best[idx] {
grid.set_material(x, y, MaterialId::Stone);
}
}
}
}
fn flood_fill(
&self,
grid: &ChunkedGrid,
visited: &mut [bool],
x: i32,
y: i32,
comp: &mut Vec<(i32, i32)>,
) {
let w = grid.width as i32;
let mut stack = vec![(x, y)];
while let Some((cx, cy)) = stack.pop() {
let idx = (cy * w + cx) as usize;
if visited[idx] || !grid.in_bounds(cx, cy) || !grid.get(cx, cy).is_empty() {
continue;
}
visited[idx] = true;
comp.push((cx, cy));
for &(dx, dy) in &NEIGHBORS4 {
stack.push((cx + dx, cy + dy));
}
}
}
fn build_bsp(&mut self, rect: Rect) -> BspNode {
let mut node = BspNode {
left: None,
right: None,
room: None,
};
if let Some((left, right)) = self.split_rect(rect) {
node.left = Some(Box::new(self.build_bsp(left)));
node.right = Some(Box::new(self.build_bsp(right)));
} else {
node.room = Some(self.carve_room_rect(rect));
}
node
}
fn split_rect(&mut self, rect: Rect) -> Option<(Rect, Rect)> {
let min_size = sc(9);
if rect.w < min_size * 2 || rect.h < min_size * 2 {
return None;
}
if rect.w > rect.h {
let split = self.random_range_i32(min_size, rect.w - min_size);
let left = Rect {
x: rect.x,
y: rect.y,
w: split,
h: rect.h,
};
let right = Rect {
x: rect.x + split,
y: rect.y,
w: rect.w - split,
h: rect.h,
};
Some((left, right))
} else {
let split = self.random_range_i32(min_size, rect.h - min_size);
let top = Rect {
x: rect.x,
y: rect.y,
w: rect.w,
h: split,
};
let bottom = Rect {
x: rect.x,
y: rect.y + split,
w: rect.w,
h: rect.h - split,
};
Some((top, bottom))
}
}
fn carve_room_rect(&mut self, rect: Rect) -> Rect {
let min_w = sc(4);
let min_h = sc(4);
let max_pad_x = ((rect.w - min_w) / 2).max(1);
let max_pad_y = ((rect.h - min_h) / 2).max(1);
let pad_x = self.random_range_i32(1, max_pad_x + 1);
let pad_y = self.random_range_i32(1, max_pad_y + 1);
Rect {
x: rect.x + pad_x,
y: rect.y + pad_y,
w: rect.w - pad_x * 2,
h: rect.h - pad_y * 2,
}
}
fn carve_room(&mut self, grid: &mut ChunkedGrid, room: Rect) {
for y in room.y..room.y + room.h {
for x in room.x..room.x + room.w {
grid.set(x, y, Cell::empty());
}
}
}
fn collect_rooms(&self, node: &BspNode, rooms: &mut Vec<Rect>) {
if let Some(room) = node.room {
rooms.push(room);
}
if let Some(ref left) = node.left {
self.collect_rooms(left, rooms);
}
if let Some(ref right) = node.right {
self.collect_rooms(right, rooms);
}
}
fn connect_bsp_rooms(&mut self, node: &BspNode, grid: &mut ChunkedGrid) {
if let (Some(left), Some(right)) = (&node.left, &node.right) {
let r1 = self.find_first_room(left);
let r2 = self.find_first_room(right);
if let (Some(a), Some(b)) = (r1, r2) {
self.carve_l_corridor(grid, &a, &b);
}
self.connect_bsp_rooms(left, grid);
self.connect_bsp_rooms(right, grid);
}
}
fn find_first_room(&self, node: &BspNode) -> Option<Rect> {
if let Some(room) = node.room {
return Some(room);
}
if let Some(ref left) = node.left {
if let Some(room) = self.find_first_room(left) {
return Some(room);
}
}
if let Some(ref right) = node.right {
if let Some(room) = self.find_first_room(right) {
return Some(room);
}
}
None
}
fn carve_l_corridor(&mut self, grid: &mut ChunkedGrid, a: &Rect, b: &Rect) {
let c1 = (a.x + a.w / 2, a.y + a.h / 2);
let c2 = (b.x + b.w / 2, b.y + b.h / 2);
let cw = sc(2) / 2;
let x0 = c1.0.min(c2.0);
let x1 = c1.0.max(c2.0);
for x in x0..=x1 {
for dy in 0..cw {
if grid.in_bounds(x, c1.1 + dy) {
grid.set(x, c1.1 + dy, Cell::empty());
}
}
}
let y0 = c1.1.min(c2.1);
let y1 = c1.1.max(c2.1);
for y in y0..=y1 {
for dx in 0..cw {
if grid.in_bounds(c2.0 + dx, y) {
grid.set(c2.0 + dx, y, Cell::empty());
}
}
}
}
fn find_safe_spawn(&mut self, grid: &ChunkedGrid) -> (i32, i32) {
let w = grid.width as i32;
let h = grid.height as i32;
for floor_y in 10..h - 10 {
for x in 10..w - 10 {
if !grid.get(x, floor_y).is_solid() {
continue;
}
let cy = floor_y - 3;
if !self.vertical_clear(grid, x, cy) {
continue;
}
if !self.horizontal_clear(grid, x, cy) {
continue;
}
return (x, cy);
}
}
(w / 2, h / 2)
}
fn find_empty_with_floor(
&mut self,
grid: &ChunkedGrid,
target_x: i32,
target_y: i32,
) -> Option<(i32, i32)> {
let w = grid.width as i32;
let h = grid.height as i32;
let mut best = None;
let mut best_dist = i32::MAX;
for y in 5..h - 5 {
for x in 5..w - 5 {
if grid.get(x, y).is_empty()
&& grid.in_bounds(x, y + 1)
&& grid.get(x, y + 1).is_solid()
{
let d = (x - target_x).abs() + (y - target_y).abs();
if d < best_dist {
best_dist = d;
best = Some((x, y));
}
}
}
}
best
}
fn vertical_clear(&self, grid: &ChunkedGrid, x: i32, y: i32) -> bool {
for dy in -4..=2 {
if !grid.in_bounds(x, y + dy) || !grid.get(x, y + dy).is_empty() {
return false;
}
}
grid.in_bounds(x, y + 3) && grid.get(x, y + 3).is_solid()
}
fn horizontal_clear(&self, grid: &ChunkedGrid, x: i32, y: i32) -> bool {
for dx in -3..=3 {
if !grid.in_bounds(x + dx, y) || !grid.get(x + dx, y).is_empty() {
return false;
}
}
true
}
fn fallback_spawn(&mut self, grid: &mut ChunkedGrid) -> (i32, i32) {
let w = grid.width as i32;
let h = grid.height as i32;
for y in (10..h - 10).rev() {
for x in 10..w - 10 {
if grid.get(x, y).is_empty() {
grid.set(x, y, Cell::empty());
return (x, y);
}
}
}
(w / 2, h / 2)
}
fn surface_height(&mut self, x: i32, h: i32) -> i32 {
let base = (h - 3) - ((x as f32 * 0.012).sin() * scf(3.0)) as i32;
let detail = ((x as f32 * 0.04).sin() * scf(1.5)) as i32;
let micro = ((x as f32 * 0.11).sin() * scf(0.5)) as i32;
(base + detail + micro).max(10).min(h - 3)
}
const SURFACE_BASE_Y: i32 = 120;
fn surface_height_world(&mut self, x: i32) -> i32 {
let base = Self::SURFACE_BASE_Y - ((x as f32 * 0.012).sin() * scf(3.0)) as i32;
let detail = ((x as f32 * 0.04).sin() * scf(1.5)) as i32;
let micro = ((x as f32 * 0.11).sin() * scf(0.5)) as i32;
(base + detail + micro).max(10)
}
fn find_surface_near(&mut self, grid: &ChunkedGrid, x: i32) -> i32 {
let h = grid.height as i32;
for y in 0..h {
if grid.get(x, y).is_solid() && grid.get(x, y).material != MaterialId::Stone {
return y;
}
}
h - 3
}
fn random_surface_pool_type(&mut self, depth: u32) -> MaterialId {
let r = self.ca.random_u32() % 100;
if depth == 1 {
match r {
0..=50 => MaterialId::Water,
51..=80 => MaterialId::Sand,
_ => MaterialId::Acid,
}
} else {
match r {
0..=40 => MaterialId::Water,
41..=60 => MaterialId::Sand,
61..=80 => MaterialId::Acid,
_ => MaterialId::Lava,
}
}
}
fn random_cave_pool_type(&mut self, depth: u32) -> MaterialId {
let r = self.ca.random_u32() % 100;
match depth {
4 => match r {
0..=50 => MaterialId::Water,
51..=75 => MaterialId::Lava,
_ => MaterialId::Acid,
},
_ => match r {
0..=30 => MaterialId::Water,
31..=60 => MaterialId::Lava,
_ => MaterialId::Acid,
},
}
}
fn random_item_type(&mut self) -> Option<ItemType> {
let types = [
ItemType::Dagger,
ItemType::Sword,
ItemType::Bow,
ItemType::LeatherArmor,
ItemType::PlateArmor,
ItemType::Shield,
ItemType::HealthPotion,
ItemType::ManaPotion,
ItemType::Food,
ItemType::Scroll,
];
let idx = self.random_usize(types.len());
Some(types[idx])
}
fn random_point_in_room(&mut self, room: Rect) -> (i32, i32) {
let x = room.x + self.random_range_i32(1, room.w - 1);
let y = room.y + self.random_range_i32(1, room.h - 1);
(x, y)
}
fn shuffle_rooms(&mut self, rooms: &mut Vec<Rect>) {
for i in (1..rooms.len()).rev() {
let j = self.random_usize(i + 1);
rooms.swap(i, j);
}
}
fn random_range_i32(&mut self, min: i32, max: i32) -> i32 {
if max <= min {
return min;
}
min + (self.ca.random_u32() % (max - min) as u32) as i32
}
fn random_usize(&mut self, max: usize) -> usize {
if max == 0 {
return 0;
}
self.ca.random_usize(max)
}
}
struct BspNode {
left: Option<Box<BspNode>>,
right: Option<Box<BspNode>>,
room: Option<Rect>,
}
const NEIGHBORS4: [(i32, i32); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];