summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-30 18:42:16 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-30 18:42:16 -0700
commit56bba5560be95c1e05bfec11e4fc085654f3589d (patch)
treef4eee7eb7abdce9ab6d0a7a66e3e43d429229b97
parenta0e3b22efb1d1a8b885717481061074459fe109f (diff)
working with tilesets
-rw-r--r--assets/tilesets/tileset1/img.pngbin0 -> 860 bytes
-rw-r--r--assets/tilesets/tileset1/manifest.toml7
-rw-r--r--assets/tilesets/tileset_test/img.pngbin0 -> 1167 bytes
-rw-r--r--assets/tilesets/tileset_test/manifest.toml7
-rw-r--r--src/config.rs2
-rw-r--r--src/content/mod.rs1
-rw-r--r--src/content/world/mines.rs0
-rw-r--r--src/content/world/mod.rs1
-rw-r--r--src/main.rs14
-rw-r--r--src/proc_gen/herringbone.rs83
-rw-r--r--src/proc_gen/mod.rs114
-rw-r--r--src/proc_gen/tileset_loader.rs204
-rw-r--r--src/renderer/mod.rs4
-rw-r--r--src/renderer/ui.rs45
-rw-r--r--src/sim/entity/mod.rs2
15 files changed, 340 insertions, 144 deletions
diff --git a/assets/tilesets/tileset1/img.png b/assets/tilesets/tileset1/img.png
new file mode 100644
index 0000000..e730faf
--- /dev/null
+++ b/assets/tilesets/tileset1/img.png
Binary files differ
diff --git a/assets/tilesets/tileset1/manifest.toml b/assets/tilesets/tileset1/manifest.toml
new file mode 100644
index 0000000..8edea7b
--- /dev/null
+++ b/assets/tilesets/tileset1/manifest.toml
@@ -0,0 +1,7 @@
+[dimensions]
+short = 22
+long = 44
+
+[palette]
+0 = "Void"
+1 = "Terrain" \ No newline at end of file
diff --git a/assets/tilesets/tileset_test/img.png b/assets/tilesets/tileset_test/img.png
new file mode 100644
index 0000000..437e6c3
--- /dev/null
+++ b/assets/tilesets/tileset_test/img.png
Binary files differ
diff --git a/assets/tilesets/tileset_test/manifest.toml b/assets/tilesets/tileset_test/manifest.toml
new file mode 100644
index 0000000..8edea7b
--- /dev/null
+++ b/assets/tilesets/tileset_test/manifest.toml
@@ -0,0 +1,7 @@
+[dimensions]
+short = 22
+long = 44
+
+[palette]
+0 = "Void"
+1 = "Terrain" \ No newline at end of file
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
}