pub mod herringbone; pub mod tileset_loader; use std::{ cmp::Ordering, collections::HashMap, ops::{Add, Mul, Sub}, }; use fxhash::FxHashMap; use glam::IVec2; use crate::{ config::{CHUNK_SIZE, TILESET_SCALING}, proc_gen::{ herringbone::{split_position, variant_index}, tileset_loader::{Tile, TileOrientation, TilePixelType, Tileset, load_tileset}, }, sim::{cell::Cell, cell_manager::chunk::Chunk, entity::EntityDef}, }; #[inline] fn lerp(a: T, b: T, t: f32) -> T where T: Copy + Add + Sub + Mul, { a + (b - a) * t } const CHUNK_PIXELS: i32 = CHUNK_SIZE / TILESET_SCALING; const PIXELS_NEIGHBOURHOOD_SIZE: i32 = CHUNK_PIXELS + 2; // 2d slice of solidity; chunk + 1 margin struct BiomeChunkContext { pixels: [TilePixelType; (PIXELS_NEIGHBOURHOOD_SIZE * PIXELS_NEIGHBOURHOOD_SIZE) as usize], } pub struct InterpolatedPixel { pub interpolated_pixel_index: u8, pub interpolated_solidity: f32, } impl BiomeChunkContext { fn interpolated_at(&self, local: IVec2, tileset: &Tileset) -> InterpolatedPixel { // pixel coord centre of cell coord local let pixel_position = (local.as_vec2() + 0.5) / TILESET_SCALING as f32 + 0.5; let pixel = pixel_position.floor(); // distance from the current pixel to the centre of the cell let frac = pixel_position - pixel; let pixel = pixel.as_ivec2(); let instantaneous_pixel_at = |dx, dy| { self.pixels[((pixel.x + dx) + (pixel.y + dy) * PIXELS_NEIGHBOURHOOD_SIZE) as usize] }; let instantaneous_idx_at = |dx, dy| match instantaneous_pixel_at(dx, dy) { TilePixelType::Void => 255, TilePixelType::Terrain(_) => 0, TilePixelType::Custom(i) => i, }; let instantaneous_solidity_at = |dx, dy| match instantaneous_pixel_at(dx, dy) { TilePixelType::Void => 0.0, TilePixelType::Terrain(o) => o, TilePixelType::Custom(i) => { tileset.manifest.palette.get(&i).map_or(0.0, |p| p.solidity) } }; // linearly interpolate between the solidity of the horizontal cells above and below according to the fractional x component let s_top = lerp( instantaneous_solidity_at(0, 0), instantaneous_solidity_at(1, 0), frac.x, ); let s_bottom = lerp( instantaneous_solidity_at(0, 1), instantaneous_solidity_at(1, 1), frac.x, ); // and then interpolate between those according to the y component let interpolated_solidity = lerp(s_top, s_bottom, frac.y) - 0.5; // same for the pixel // TODO don't allocate per-cell let mut idxes: Vec = Vec::new(); let tl = instantaneous_idx_at(0, 0); if tl != 255 && !idxes.contains(&tl) { idxes.push(tl); } let tr = instantaneous_idx_at(1, 0); if tr != 255 && !idxes.contains(&tr) { idxes.push(tr); } let bl = instantaneous_idx_at(0, 1); if bl != 255 && !idxes.contains(&bl) { idxes.push(bl); } let br = instantaneous_idx_at(1, 1); if br != 255 && !idxes.contains(&br) { idxes.push(br); } let idx_strengths = idxes.iter().map(|&i| { let i_top = lerp( if tl == i { 1.0 } else { 0.0 }, if tr == i { 1.0 } else { 0.0 }, frac.x, ); let i_bottom = lerp( if bl == i { 1.0 } else { 0.0 }, if br == i { 1.0 } else { 0.0 }, frac.x, ); let interpolated_strength = lerp(i_top, i_bottom, frac.y); (i, interpolated_strength) }); InterpolatedPixel { interpolated_pixel_index: idx_strengths .max_by(|&(_, s1), &(_, s2)| s1.total_cmp(&s2)) .map_or(255, |(i, _)| i), interpolated_solidity, } } } pub trait Biome { fn fragment(&self, pixel: InterpolatedPixel, world: IVec2) -> Cell; fn derive_entities_from_tile(&self, _tile: &Tile) -> Vec { Vec::new() } } pub struct TilesetWorldGenerator { tileset: Tileset, biome: Box, } impl TilesetWorldGenerator { pub fn new(tileset_path: &str, biome: Box) -> Self { TilesetWorldGenerator { tileset: load_tileset(tileset_path), biome, } } fn pixel_at(&self, pixel: IVec2) -> TilePixelType { let (grid, local, orientation) = split_position(pixel, self.tileset.manifest.dimensions.short as i32); let tiles = match orientation { TileOrientation::Horizontal => &self.tileset.horizontal_tiles, TileOrientation::Vertical => &self.tileset.vertical_tiles, }; tiles[variant_index(grid, tiles.len())].pixel_at(local) } fn get_biome_chunk_context(&self, chunk_position: IVec2) -> BiomeChunkContext { let origin = chunk_position * CHUNK_PIXELS - 1; let mut pixels = [TilePixelType::Void; (PIXELS_NEIGHBOURHOOD_SIZE * PIXELS_NEIGHBOURHOOD_SIZE) as usize]; for y in 0..PIXELS_NEIGHBOURHOOD_SIZE { for x in 0..PIXELS_NEIGHBOURHOOD_SIZE { pixels[(x + y * PIXELS_NEIGHBOURHOOD_SIZE) as usize] = self.pixel_at(origin + IVec2::new(x, y)); } } BiomeChunkContext { pixels } } pub fn generate_chunk(&self, chunk_position: IVec2) -> Chunk { let mut chunk = Chunk::void(); let biome_chunk_context = self.get_biome_chunk_context(chunk_position); let chunk_min = chunk_position * CHUNK_SIZE; for y in 0..CHUNK_SIZE { for x in 0..CHUNK_SIZE { let local = IVec2::new(x, y); chunk.cells[(x + y * CHUNK_SIZE) as usize] = self.biome.fragment( biome_chunk_context.interpolated_at(local, &self.tileset), chunk_min + local, ); } } chunk.sleeping = false; chunk.mark_collider_dirty(0); chunk } }