diff options
| author | Kai Stevenson <kai@kaistevenson.com> | 2026-08-30 18:42:16 -0700 |
|---|---|---|
| committer | Kai Stevenson <kai@kaistevenson.com> | 2026-08-30 18:42:16 -0700 |
| commit | 56bba5560be95c1e05bfec11e4fc085654f3589d (patch) | |
| tree | f4eee7eb7abdce9ab6d0a7a66e3e43d429229b97 /src | |
| parent | a0e3b22efb1d1a8b885717481061074459fe109f (diff) | |
working with tilesets
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 2 | ||||
| -rw-r--r-- | src/content/mod.rs | 1 | ||||
| -rw-r--r-- | src/content/world/mines.rs | 0 | ||||
| -rw-r--r-- | src/content/world/mod.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 14 | ||||
| -rw-r--r-- | src/proc_gen/herringbone.rs | 83 | ||||
| -rw-r--r-- | src/proc_gen/mod.rs | 114 | ||||
| -rw-r--r-- | src/proc_gen/tileset_loader.rs | 204 | ||||
| -rw-r--r-- | src/renderer/mod.rs | 4 | ||||
| -rw-r--r-- | src/renderer/ui.rs | 45 | ||||
| -rw-r--r-- | src/sim/entity/mod.rs | 2 |
11 files changed, 326 insertions, 144 deletions
diff --git a/src/config.rs b/src/config.rs index 67669ab..2b60c50 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,3 +18,5 @@ pub const SETTLED_THRESOHLD: u8 = 7; // any entity with fewer cells than this will be turned to particles pub const MIN_ENTITY_CELLS: usize = 9; + +pub const TILESET_SCALING: i32 = 8; diff --git a/src/content/mod.rs b/src/content/mod.rs index ee2f47c..3293c6f 100644 --- a/src/content/mod.rs +++ b/src/content/mod.rs @@ -1,3 +1,4 @@ pub mod entities; pub mod materials; pub mod vfx; +pub mod world; diff --git a/src/content/world/mines.rs b/src/content/world/mines.rs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/content/world/mines.rs diff --git a/src/content/world/mod.rs b/src/content/world/mod.rs new file mode 100644 index 0000000..f12fc63 --- /dev/null +++ b/src/content/world/mod.rs @@ -0,0 +1 @@ +pub mod mines; diff --git a/src/main.rs b/src/main.rs index 63ed48f..0dd725d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,7 @@ use crate::{ Input::{self}, InputManager, }, - proc_gen::{WorldGenerator, WorldGeneratorConfig}, + proc_gen::TilesetWorldGenerator, renderer::RendererState, sim::{entity::EntityDef, rb_manager::DebugRenderMode, sim_manager::SimManager}, vfx::VfxManager, @@ -48,9 +48,8 @@ struct Config { cells_render: bool, debug_render: bool, debug_render_mode: DebugRenderMode, - // proc gen - world_generator_config: WorldGeneratorConfig, + // world_generator_config: WorldGeneratorConfig, } struct Diagnostics { @@ -68,7 +67,7 @@ struct App { camera: Option<Camera>, vfx_manager: Option<VfxManager>, sim_manager: Option<SimManager>, - world_generator: Option<WorldGenerator>, + world_generator: TilesetWorldGenerator, // renderer last_render: Instant, @@ -137,8 +136,6 @@ impl Default for App { cells_render: true, debug_render: false, debug_render_mode: DebugRenderMode::default(), - - world_generator_config: WorldGeneratorConfig::default(), }; Self { window: None, @@ -150,7 +147,7 @@ impl Default for App { vfx_manager: Some(VfxManager::new()), sim_manager: Some(SimManager::new()), - world_generator: Some(WorldGenerator::new(config.world_generator_config)), + world_generator: TilesetWorldGenerator::new("assets/tilesets/tileset1"), last_render: Instant::now(), @@ -237,7 +234,6 @@ impl ApplicationHandler for App { && let Some(sim) = &mut self.sim_manager && let Some(vfx) = &mut self.vfx_manager && let Some(camera) = &mut self.camera - && let Some(world_generator) = &mut self.world_generator { renderer_state.render( sim, @@ -246,7 +242,7 @@ impl ApplicationHandler for App { &mut self.config, &self.diagnostics, &self.input_manager, - world_generator, + &self.world_generator, ); vfx.after_render(delta_time); diff --git a/src/proc_gen/herringbone.rs b/src/proc_gen/herringbone.rs new file mode 100644 index 0000000..5e930ec --- /dev/null +++ b/src/proc_gen/herringbone.rs @@ -0,0 +1,83 @@ +use fxhash::hash32; +use glam::IVec2; + +use crate::{ + config::TILESET_SCALING, + content::materials::MaterialId, + proc_gen::tileset_loader::{Tile, TileOrientation, Tileset, TilesetPixelType}, + sim::cell::Cell, +}; + +fn split_position(world: IVec2, tileset: &Tileset) -> (IVec2, IVec2, TileOrientation) { + let s = tileset.dimensions.short as i32; + let tx = world.x.div_euclid(s); + let ty = world.y.div_euclid(s); + let fx = world.x.rem_euclid(s); + let fy = world.y.rem_euclid(s); + + match (tx - ty).rem_euclid(4) { + // left half of horizontal tile + 0 => ( + IVec2::new(tx, ty), + IVec2::new(fx, fy), + TileOrientation::Horizontal, + ), + // right half of horizontal tile + 1 => ( + IVec2::new(tx - 1, ty), + IVec2::new(fx + s, fy), + TileOrientation::Horizontal, + ), + // top half of vertical tile + 3 => ( + IVec2::new(tx, ty), + IVec2::new(fx, fy), + TileOrientation::Vertical, + ), + // bottom half of vertical tile + 2 => ( + IVec2::new(tx, ty - 1), + IVec2::new(fx, fy + s), + TileOrientation::Vertical, + ), + _ => unreachable!(), + } +} + +// better "%" for hashes since it prefers the high bits +#[inline] +fn reduce(h: u32, n: usize) -> usize { + (((h as u128) * (n as u128)) >> 32) as usize +} + +fn pick_variant(tile_pos: IVec2, orientation: TileOrientation, tileset: &Tileset) -> &Tile { + let h = hash32(&tile_pos); + match orientation { + TileOrientation::Horizontal => { + &tileset.horizontal_tiles[reduce(h, tileset.horizontal_tiles.len())] + } + TileOrientation::Vertical => { + &tileset.vertical_tiles[reduce(h, tileset.vertical_tiles.len())] + } + } +} + +// TODO: optimize +fn get_pixel_at_position(world: IVec2, tileset: &Tileset) -> TilesetPixelType { + let scaled_world = world.div_euclid(IVec2::new(TILESET_SCALING, TILESET_SCALING)); + let (tile_pos, local, orientation) = split_position(scaled_world, tileset); + let tile = pick_variant(tile_pos, orientation, tileset); + let width = match orientation { + TileOrientation::Horizontal => tileset.dimensions.long as i32, + TileOrientation::Vertical => tileset.dimensions.short as i32, + }; + tile.pixels[(local.x + local.y * width) as usize] +} + +pub fn get_cell_at_position(world: IVec2, tileset: &Tileset) -> Cell { + let pixel = get_pixel_at_position(world, tileset); + match pixel { + TilesetPixelType::Void => Cell::void(), + TilesetPixelType::Terrain => Cell::from_material(MaterialId::Dirt), + } +} diff --git a/src/proc_gen/mod.rs b/src/proc_gen/mod.rs index fb6a326..7913d22 100644 --- a/src/proc_gen/mod.rs +++ b/src/proc_gen/mod.rs @@ -1,101 +1,37 @@ -use fastnoise_lite::{FastNoiseLite, NoiseType}; -use glam::{IVec2, Vec2}; +pub mod herringbone; +pub mod tileset_loader; + +use glam::IVec2; use crate::{ config::CHUNK_SIZE, - content::materials::MaterialId, - sim::{cell::Cell, cell_manager::chunk::Chunk}, + proc_gen::{ + herringbone::get_cell_at_position, + tileset_loader::{Tileset, load_tileset}, + }, + sim::cell_manager::chunk::Chunk, }; -#[derive(Clone, Copy)] -pub struct WorldGeneratorConfig { - pub occupied_threshold: f32, - pub stone_threshold: f32, - pub proc_gen_seed: i32, - - pub cave_noise_freq: f32, - pub cave_noise_type: NoiseType, - pub hardness_noise_freq: f32, - pub hardness_noise_type: NoiseType, -} - -impl Default for WorldGeneratorConfig { - fn default() -> Self { - Self { - occupied_threshold: 0.05, - stone_threshold: 0.6, - proc_gen_seed: 10, - - cave_noise_freq: 0.005, - cave_noise_type: NoiseType::OpenSimplex2S, - hardness_noise_freq: 0.015, - hardness_noise_type: NoiseType::OpenSimplex2S, - } - } -} - -pub struct WorldGenerator { - config: WorldGeneratorConfig, - cave_noise: Option<FastNoiseLite>, - hardness_noise: Option<FastNoiseLite>, +pub struct TilesetWorldGenerator { + tileset: Tileset, } -impl WorldGenerator { - pub fn generate_chunk(&mut self, chunk_position: IVec2) -> Chunk { - if let Some(cave_noise) = &self.cave_noise - && let Some(hardness_noise) = &self.hardness_noise - { - let mut c = Chunk::void(); - let position = chunk_position * CHUNK_SIZE; - for x in 0..CHUNK_SIZE { - for y in 0..CHUNK_SIZE { - let cell_position = position.as_vec2() + Vec2::new(x as f32, y as f32); - let cnv = cave_noise.get_noise_2d(cell_position.x, cell_position.y); - let hnv = hardness_noise.get_noise_2d(cell_position.x, cell_position.y); - - let occupied = cnv < self.config.occupied_threshold; - if occupied { - let material = if hnv > self.config.stone_threshold { - MaterialId::Stone - } else { - MaterialId::Dirt - }; - c.set_cell_at_local_position( - x as u8, - y as u8, - Cell::from_material(material), - ); - } - } +impl TilesetWorldGenerator { + pub fn generate_chunk(&self, chunk_position: IVec2) -> Chunk { + let mut chunk = Chunk::void(); + for x in 0..CHUNK_SIZE { + for y in 0..CHUNK_SIZE { + let cell = get_cell_at_position( + chunk_position * CHUNK_SIZE + IVec2::new(x, y), + &self.tileset, + ); + chunk.set_cell_at_local_position(x as u8, y as u8, cell); } - c - } else { - panic!("Call update_params first"); } + chunk } - - pub fn update_params(&mut self, config: WorldGeneratorConfig) { - let mut cave_noise = FastNoiseLite::with_seed(config.proc_gen_seed + 50); - cave_noise.set_frequency(Some(config.cave_noise_freq)); - cave_noise.set_noise_type(Some(config.cave_noise_type)); - - let mut hardness_noise = FastNoiseLite::with_seed(config.proc_gen_seed + 100); - hardness_noise.set_frequency(Some(config.hardness_noise_freq)); - hardness_noise.set_noise_type(Some(config.hardness_noise_type)); - - self.cave_noise = Some(cave_noise); - self.hardness_noise = Some(hardness_noise); - - self.config = config; - } - - pub fn new(config: WorldGeneratorConfig) -> Self { - let mut wg = WorldGenerator { - config, - cave_noise: None, - hardness_noise: None, - }; - wg.update_params(config); - wg + pub fn new(tileset_path: &str) -> Self { + let tileset = load_tileset(tileset_path); + TilesetWorldGenerator { tileset } } } diff --git a/src/proc_gen/tileset_loader.rs b/src/proc_gen/tileset_loader.rs new file mode 100644 index 0000000..925b3ac --- /dev/null +++ b/src/proc_gen/tileset_loader.rs @@ -0,0 +1,204 @@ +use serde::Deserialize; +use std::{collections::HashMap, error::Error, fs::File, io::BufReader, path::Path}; + +fn load_img(path: &Path) -> Result<(Vec<u8>, u32, u32), Box<dyn Error>> { + let mut decoder = png::Decoder::new(BufReader::new(File::open(path)?)); + decoder.set_transformations(png::Transformations::IDENTITY); + + let mut reader = decoder.read_info()?; + // only None if it's too big for memory + let mut buf = vec![0; reader.output_buffer_size().unwrap()]; + let info = reader.next_frame(&mut buf)?; + + if info.color_type != png::ColorType::Indexed { + return Err(format!("{:#?}: not indexed", path.to_str()).into()); + } + if info.bit_depth != png::BitDepth::Eight { + return Err(format!( + "{:#?}: expected 8-bit, got {:?}", + path.to_str(), + info.bit_depth + ) + .into()); + } + + buf.truncate(info.buffer_size()); + Ok((buf, info.width, info.height)) +} + +#[derive(Deserialize, Debug)] +pub struct TilesetDimensions { + pub short: u32, + pub long: u32, +} + +#[derive(Deserialize, Clone, Copy, Debug)] +pub enum TilesetPixelType { + Void = 0, + Terrain, +} + +#[derive(Deserialize, Debug)] +struct TilesetManifest { + dimensions: TilesetDimensions, + palette: HashMap<u8, TilesetPixelType>, +} + +fn load_manifest(path: &Path) -> Result<TilesetManifest, Box<dyn Error>> { + Ok(toml::from_str(&std::fs::read_to_string(path)?)?) +} + +#[derive(Debug)] +pub struct Tile { + pub pixels: Vec<TilesetPixelType>, +} + +#[derive(Debug)] +pub struct Tileset { + pub dimensions: TilesetDimensions, + pub vertical_tiles: Vec<Tile>, + pub horizontal_tiles: Vec<Tile>, +} + +#[derive(Debug, Clone, Copy)] +pub enum TileOrientation { + Horizontal, + Vertical, +} + +fn parse_row( + y: u32, + img: &(Vec<u8>, u32, u32), + manifest: &TilesetManifest, + orientation: TileOrientation, +) -> Result<Vec<Tile>, String> { + let ey = match orientation { + TileOrientation::Horizontal => y + manifest.dimensions.short, + TileOrientation::Vertical => y + manifest.dimensions.long, + }; + let mut tiles = Vec::new(); + let mut x = 1; + loop { + if x >= img.1 { + break; + } + let left_border = img.0[(x - 1 + y * img.1) as usize]; + // 2 = border color + if left_border != 2 { + break; + } + + let mut tile_pixels = Vec::new(); + let ex = match orientation { + TileOrientation::Horizontal => x + manifest.dimensions.long, + TileOrientation::Vertical => x + manifest.dimensions.short, + }; + + for py in y..ey { + for px in x..ex { + let idx = img.0[(px + py * img.1) as usize]; + let palette = manifest.palette.get(&idx); + + match palette { + Some(pixel_type) => tile_pixels.push(*pixel_type), + None => { + return Err(format!( + "Pixel ({px},{py}) resolved to idx {idx} which isn't in the palette" + )); + } + } + } + } + + debug_assert!( + tile_pixels.len() == (manifest.dimensions.long * manifest.dimensions.short) as usize + ); + + tiles.push(Tile { + pixels: tile_pixels, + }); + + x = ex + 3; + } + Ok(tiles) +} + +fn parse_tiles( + img: &(Vec<u8>, u32, u32), + manifest: &TilesetManifest, +) -> Result<(Vec<Tile>, Vec<Tile>), String> { + let mut horizontal_tiles = Vec::new(); + let mut vertical_tiles = Vec::new(); + let mut y = 0; + loop { + if y >= img.2 { + break; + } + + let edge = img.0[((y) * img.1) as usize] == 2; + if !edge { + y += 1; + continue; + } + + let orientation = if img.0[((y + manifest.dimensions.short + 2) * img.1) as usize] == 0 { + TileOrientation::Horizontal + } else { + TileOrientation::Vertical + }; + + let parse_result = parse_row(y + 1, img, manifest, orientation); + match parse_result { + Ok(mut tiles) => match orientation { + TileOrientation::Horizontal => { + println!("Parsed horizontal row {y}. {n} entries", n = tiles.len()); + horizontal_tiles.append(&mut tiles); + y += manifest.dimensions.short + 3; + } + TileOrientation::Vertical => { + println!("Parsed vertical row {y}. {n} entries", n = tiles.len()); + vertical_tiles.append(&mut tiles); + y += manifest.dimensions.long + 3; + } + }, + Err(e) => { + return Err(format!( + "Failed to parse row {} ({:#?}): {e}", + y + 1, + orientation + )); + } + } + } + + Ok((horizontal_tiles, vertical_tiles)) +} + +pub fn load_tileset(path: &str) -> Tileset { + let img_result = load_img(&Path::join(Path::new(path), Path::new("img.png"))); + match img_result { + Ok(img_result) => { + let manifest_result = + load_manifest(&Path::join(Path::new(path), Path::new("manifest.toml"))); + match manifest_result { + Ok(manifest) => { + debug_assert!(manifest.dimensions.long == manifest.dimensions.short * 2); + let (horizontal_tiles, vertical_tiles) = + parse_tiles(&img_result, &manifest).unwrap(); + + Tileset { + dimensions: manifest.dimensions, + vertical_tiles, + horizontal_tiles, + } + } + Err(e) => { + panic!("Couldn't load sprite: {}", e) + } + } + } + Err(e) => { + panic!("Couldn't load sprite: {}", e) + } + } +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index b5508e2..aca2b17 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -10,7 +10,7 @@ use crate::{ camera::Camera, config::{CELLS_IN_CHUNK, CHUNK_SIZE}, content::{materials::MaterialId, vfx::VfxMaterialId}, - proc_gen::WorldGenerator, + proc_gen::TilesetWorldGenerator, renderer::ui::draw_egui, sim::{ cell_manager::manager::CellManager, entity::EntityId, @@ -773,7 +773,7 @@ impl RendererState { config: &mut Config, diagnostics: &Diagnostics, input_manager: &InputManager, - world_generator: &mut WorldGenerator, + world_generator: &TilesetWorldGenerator, ) { puffin::profile_function!(); diff --git a/src/renderer/ui.rs b/src/renderer/ui.rs index 98c916d..1ef1bd1 100644 --- a/src/renderer/ui.rs +++ b/src/renderer/ui.rs @@ -1,5 +1,4 @@ use egui::{Color32, Stroke, Ui, epaint::CircleShape}; -use fastnoise_lite::NoiseType; use glam::IVec2; use rapier2d::pipeline::QueryFilter; @@ -7,7 +6,7 @@ use crate::{ Camera, Config, Diagnostics, content::materials::MaterialId, input::InputManager, - proc_gen::WorldGenerator, + proc_gen::TilesetWorldGenerator, sim::{rb_manager::DebugRenderMode, sim_manager::SimManager}, }; @@ -27,7 +26,7 @@ pub fn draw_egui<'a>( diagnostics: &Diagnostics, input_manager: &InputManager, sim: &mut SimManager, - world_generator: &mut WorldGenerator, + world_generator: &TilesetWorldGenerator, ) { puffin::profile_function!(); ui.heading("Config"); @@ -110,7 +109,6 @@ pub fn draw_egui<'a>( ui.add(egui::Slider::new(&mut camera.zoom, 0.0..=10.0).text("Zoom")); ui.heading("Proc gen"); if ui.button("Generate").clicked() { - world_generator.update_params(config.world_generator_config); for cx in -5..5 { for cy in -5..5 { let chunk = world_generator.generate_chunk(IVec2::new(cx, cy)); @@ -118,45 +116,6 @@ pub fn draw_egui<'a>( } } } - ui.add( - egui::Slider::new( - &mut config.world_generator_config.proc_gen_seed, - -100000..=100000, - ) - .text("Seed"), - ); - ui.add( - egui::Slider::new( - &mut config.world_generator_config.occupied_threshold, - 0.0..=1.0, - ) - .text("Occupation threshold"), - ); - ui.add( - egui::Slider::new( - &mut config.world_generator_config.stone_threshold, - 0.0..=1.0, - ) - .text("Stone threshold"), - ); - - ui.label("Cave noise"); - ui.add( - egui::Slider::new( - &mut config.world_generator_config.cave_noise_freq, - 0.0..=1.0, - ) - .text("Frequency"), - ); - - ui.label("Hardness noise"); - ui.add( - egui::Slider::new( - &mut config.world_generator_config.hardness_noise_freq, - 0.0..=1.0, - ) - .text("Frequency"), - ); ui.label(format!( "Mouse (world): x,y=({x}, {y})", diff --git a/src/sim/entity/mod.rs b/src/sim/entity/mod.rs index 3f262cb..2f4034b 100644 --- a/src/sim/entity/mod.rs +++ b/src/sim/entity/mod.rs @@ -16,7 +16,7 @@ use crate::{ fn mass_from_cells(cells: &[Cell]) -> f32 { cells .iter() - .fold(0, |acc, cur| acc + cur.material.def().density) as f32 + .fold(0.0, |acc, cur| acc + cur.material.def().density as f32) * MASS_SCALING } |
