1pub mod biome;
2pub mod block;
3pub mod chonk;
4pub mod map;
5pub mod site;
6pub mod sprite;
7pub mod structure;
8
9use std::ops::{Add, Mul};
10
11pub use self::{
13 biome::BiomeKind,
14 block::{Block, BlockKind},
15 map::MapSizeLg,
16 site::SiteKindMeta,
17 sprite::{SpriteCfg, SpriteKind, StructureSprite, UnlockKind},
18 structure::{Structure, StructuresGroup},
19};
20use hashbrown::HashMap;
21use roots::find_roots_cubic;
22use serde::{Deserialize, Serialize};
23
24use crate::{
25 vol::{ReadVol, RectVolSize},
26 volumes::vol_grid_2d::VolGrid2d,
27};
28use vek::*;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct TerrainChunkSize;
34
35pub const TERRAIN_CHUNK_BLOCKS_LG: u32 = 5;
47
48impl RectVolSize for TerrainChunkSize {
49 const RECT_SIZE: Vec2<u32> = Vec2 {
50 x: (1 << TERRAIN_CHUNK_BLOCKS_LG),
51 y: (1 << TERRAIN_CHUNK_BLOCKS_LG),
52 };
53}
54
55impl TerrainChunkSize {
56 #[inline(always)]
57 pub fn blocks(chunks: Vec2<u32>) -> Vec2<u32> { chunks * Self::RECT_SIZE }
65
66 pub fn center_wpos(chunk_pos: Vec2<i32>) -> Vec2<i32> {
78 chunk_pos * Self::RECT_SIZE.as_::<i32>() + Self::RECT_SIZE.as_::<i32>() / 2
79 }
80}
81
82pub trait CoordinateConversions {
83 fn wpos_to_cpos(&self) -> Self;
84 fn cpos_to_wpos(&self) -> Self;
85 fn cpos_to_wpos_center(&self) -> Self;
86}
87
88impl CoordinateConversions for Vec2<i32> {
89 #[inline]
90 fn wpos_to_cpos(&self) -> Self {
91 self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.div_euclid(sz as i32))
92 }
93
94 #[inline]
95 fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) }
96
97 #[inline]
98 fn cpos_to_wpos_center(&self) -> Self {
99 self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
100 e * sz as i32 + sz as i32 / 2
101 })
102 }
103}
104
105impl CoordinateConversions for Vec2<f32> {
106 #[inline]
107 fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as f32) }
108
109 #[inline]
110 fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f32) }
111
112 #[inline]
113 fn cpos_to_wpos_center(&self) -> Self {
114 self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
115 e * sz as f32 + sz as f32 / 2.0
116 })
117 }
118}
119
120impl CoordinateConversions for Vec2<f64> {
121 #[inline]
122 fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as f64) }
123
124 #[inline]
125 fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f64) }
126
127 #[inline]
128 fn cpos_to_wpos_center(&self) -> Self {
129 self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
130 e * sz as f64 + sz as f64 / 2.0
131 })
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TerrainChunkMeta {
139 name: Option<String>,
140 biome: BiomeKind,
141 alt: f32,
142 tree_density: f32,
143 contains_river: bool,
144 near_water: bool,
145 river_velocity: Vec3<f32>,
146 approx_chunk_terrain_normal: Option<Vec3<f32>>,
147 rockiness: f32,
148 cliff_height: f32,
149 temp: f32,
150 humidity: f32,
151 site: Option<SiteKindMeta>,
152 tracks: Vec<CubicBezier3<f32>>,
153 debug_points: Vec<Vec3<f32>>,
154 debug_lines: Vec<LineSegment3<f32>>,
155 sprite_cfgs: HashMap<Vec3<i32>, SpriteCfg>,
156}
157
158impl TerrainChunkMeta {
159 pub fn new(
160 name: Option<String>,
161 biome: BiomeKind,
162 alt: f32,
163 tree_density: f32,
164 contains_river: bool,
165 near_water: bool,
166 river_velocity: Vec3<f32>,
167 temp: f32,
168 humidity: f32,
169 site: Option<SiteKindMeta>,
170 approx_chunk_terrain_normal: Option<Vec3<f32>>,
171 rockiness: f32,
172 cliff_height: f32,
173 ) -> Self {
174 Self {
175 name,
176 biome,
177 alt,
178 tree_density,
179 contains_river,
180 near_water,
181 river_velocity,
182 temp,
183 humidity,
184 site,
185 tracks: Vec::new(),
186 debug_points: Vec::new(),
187 debug_lines: Vec::new(),
188 sprite_cfgs: HashMap::default(),
189 approx_chunk_terrain_normal,
190 rockiness,
191 cliff_height,
192 }
193 }
194
195 pub fn void() -> Self {
196 Self {
197 name: None,
198 biome: BiomeKind::Void,
199 alt: 0.0,
200 tree_density: 0.0,
201 contains_river: false,
202 near_water: false,
203 river_velocity: Vec3::zero(),
204 temp: 0.0,
205 humidity: 0.0,
206 site: None,
207 tracks: Vec::new(),
208 debug_points: Vec::new(),
209 debug_lines: Vec::new(),
210 sprite_cfgs: HashMap::default(),
211 approx_chunk_terrain_normal: None,
212 rockiness: 0.0,
213 cliff_height: 0.0,
214 }
215 }
216
217 pub fn name(&self) -> Option<&str> { self.name.as_deref() }
218
219 pub fn biome(&self) -> BiomeKind { self.biome }
220
221 pub fn alt(&self) -> f32 { self.alt }
223
224 pub fn tree_density(&self) -> f32 { self.tree_density }
225
226 pub fn contains_river(&self) -> bool { self.contains_river }
227
228 pub fn near_water(&self) -> bool { self.near_water }
229
230 pub fn river_velocity(&self) -> Vec3<f32> { self.river_velocity }
231
232 pub fn site(&self) -> Option<SiteKindMeta> { self.site }
233
234 pub fn temp(&self) -> f32 { self.temp }
236
237 pub fn humidity(&self) -> f32 { self.humidity }
238
239 pub fn tracks(&self) -> &[CubicBezier3<f32>] { &self.tracks }
240
241 pub fn add_track(&mut self, bezier: CubicBezier3<f32>) { self.tracks.push(bezier); }
242
243 pub fn debug_points(&self) -> &[Vec3<f32>] { &self.debug_points }
244
245 pub fn add_debug_point(&mut self, point: Vec3<f32>) { self.debug_points.push(point); }
246
247 pub fn debug_lines(&self) -> &[LineSegment3<f32>] { &self.debug_lines }
248
249 pub fn add_debug_line(&mut self, line: LineSegment3<f32>) { self.debug_lines.push(line); }
250
251 pub fn sprite_cfg_at(&self, rpos: Vec3<i32>) -> Option<&SpriteCfg> {
252 self.sprite_cfgs.get(&rpos)
253 }
254
255 pub fn set_sprite_cfg_at(&mut self, rpos: Vec3<i32>, sprite_cfg: SpriteCfg) {
256 self.sprite_cfgs.insert(rpos, sprite_cfg);
257 }
258
259 pub fn approx_chunk_terrain_normal(&self) -> Option<Vec3<f32>> {
260 self.approx_chunk_terrain_normal
261 }
262
263 pub fn rockiness(&self) -> f32 { self.rockiness }
264
265 pub fn cliff_height(&self) -> f32 { self.cliff_height }
266}
267
268pub type TerrainChunk = chonk::Chonk<Block, TerrainChunkSize, TerrainChunkMeta>;
271pub type TerrainGrid = VolGrid2d<TerrainChunk>;
272
273const TERRAIN_GRID_SEARCH_DIST: i32 = 63;
274
275impl TerrainGrid {
276 pub fn find_ground(&self, pos: Vec3<i32>) -> Vec3<i32> {
279 self.try_find_ground(pos).unwrap_or(pos)
280 }
281
282 pub fn is_space(&self, pos: Vec3<i32>) -> bool {
283 (0..2).all(|z| {
284 self.get(pos + Vec3::unit_z() * z)
285 .map_or(true, |b| !b.is_solid())
286 })
287 }
288
289 pub fn try_find_space(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
290 (0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
291 .map(|i| if i % 2 == 0 { i } else { -i } / 2)
292 .map(|z_diff| pos + Vec3::unit_z() * z_diff)
293 .find(|pos| self.is_space(*pos))
294 }
295
296 pub fn try_find_ground(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
297 (0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
298 .map(|i| if i % 2 == 0 { i } else { -i } / 2)
299 .map(|z_diff| pos + Vec3::unit_z() * z_diff)
300 .find(|pos| {
301 self.get(pos - Vec3::unit_z()).is_ok_and(|b| b.is_filled()) && self.is_space(*pos)
302 })
303 }
304
305 pub fn get_interpolated<T, F>(&self, pos: Vec2<i32>, mut f: F) -> Option<T>
306 where
307 T: Copy + Default + Add<Output = T> + Mul<f32, Output = T>,
308 F: FnMut(&TerrainChunk) -> T,
309 {
310 let pos = pos.as_::<f64>().wpos_to_cpos();
311
312 let cubic = |a: T, b: T, c: T, d: T, x: f32| -> T {
313 let x2 = x * x;
314
315 let co0 = a * -0.5 + b * 1.5 + c * -1.5 + d * 0.5;
317 let co1 = a + b * -2.5 + c * 2.0 + d * -0.5;
318 let co2 = a * -0.5 + c * 0.5;
319 let co3 = b;
320
321 co0 * x2 * x + co1 * x2 + co2 * x + co3
322 };
323
324 let mut x = [T::default(); 4];
325
326 for (x_idx, j) in (-1..3).enumerate() {
327 let y0 = f(self.get_key(pos.map2(Vec2::new(j, -1), |e, q| e.max(0.0) as i32 + q))?);
328 let y1 = f(self.get_key(pos.map2(Vec2::new(j, 0), |e, q| e.max(0.0) as i32 + q))?);
329 let y2 = f(self.get_key(pos.map2(Vec2::new(j, 1), |e, q| e.max(0.0) as i32 + q))?);
330 let y3 = f(self.get_key(pos.map2(Vec2::new(j, 2), |e, q| e.max(0.0) as i32 + q))?);
331
332 x[x_idx] = cubic(y0, y1, y2, y3, pos.y.fract() as f32);
333 }
334
335 Some(cubic(x[0], x[1], x[2], x[3], pos.x.fract() as f32))
336 }
337
338 pub fn sprite_cfg_at(&self, wpos: Vec3<i32>) -> Option<&SpriteCfg> {
339 let chunk = self.pos_chunk(wpos)?;
340 let sprite_chunk_pos = TerrainGrid::chunk_offs(wpos);
341
342 chunk.meta().sprite_cfg_at(sprite_chunk_pos)
343 }
344}
345
346impl TerrainChunk {
347 pub fn water(sea_level: i32) -> TerrainChunk {
349 TerrainChunk::new(
350 sea_level,
351 Block::water(SpriteKind::Empty),
352 Block::air(SpriteKind::Empty),
353 TerrainChunkMeta::void(),
354 )
355 }
356
357 pub fn find_accessible_pos(&self, spawn_wpos: Vec2<i32>, ascending: bool) -> Vec3<f32> {
359 let min_z = self.get_min_z();
360 let max_z = self.get_max_z();
361
362 let pos = Vec3::new(
363 spawn_wpos.x,
364 spawn_wpos.y,
365 if ascending { min_z } else { max_z },
366 );
367 (0..(max_z - min_z))
368 .map(|z_diff| {
369 if ascending {
370 pos + Vec3::unit_z() * z_diff
371 } else {
372 pos - Vec3::unit_z() * z_diff
373 }
374 })
375 .find(|test_pos| {
376 let chunk_relative_xy = test_pos
377 .xy()
378 .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32));
379 self.get(
380 Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
381 - Vec3::unit_z(),
382 )
383 .is_ok_and(|b| b.is_filled())
384 && (0..3).all(|z| {
385 self.get(
386 Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
387 + Vec3::unit_z() * z,
388 )
389 .map_or(true, |b| !b.is_solid())
390 })
391 })
392 .unwrap_or(pos)
393 .map(|e| e as f32)
394 + 0.5
395 }
396}
397
398#[inline(always)]
405pub fn uniform_idx_as_vec2(map_size_lg: MapSizeLg, idx: usize) -> Vec2<i32> {
406 let x_mask = (1 << map_size_lg.vec().x) - 1;
407 Vec2::new((idx & x_mask) as i32, (idx >> map_size_lg.vec().x) as i32)
408}
409
410#[inline(always)]
414pub fn vec2_as_uniform_idx(map_size_lg: MapSizeLg, idx: Vec2<i32>) -> usize {
415 ((idx.y as usize) << map_size_lg.vec().x) | idx.x as usize
416}
417
418pub const NEIGHBOR_DELTA: [(i32, i32); 8] = [
420 (-1, -1),
421 (0, -1),
422 (1, -1),
423 (-1, 0),
424 (1, 0),
425 (-1, 1),
426 (0, 1),
427 (1, 1),
428];
429
430#[inline(always)]
432pub fn neighbors(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator<Item = usize> {
433 let pos = uniform_idx_as_vec2(map_size_lg, posi);
434 let world_size = map_size_lg.chunks();
435 NEIGHBOR_DELTA
436 .iter()
437 .map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y))
438 .filter(move |pos| {
439 pos.x >= 0 && pos.y >= 0 && pos.x < world_size.x as i32 && pos.y < world_size.y as i32
440 })
441 .map(move |pos| vec2_as_uniform_idx(map_size_lg, pos))
442}
443
444pub fn river_spline_coeffs(
445 chunk_pos: Vec2<f64>,
447 spline_derivative: Vec2<f32>,
448 downhill_pos: Vec2<f64>,
449) -> Vec3<Vec2<f64>> {
450 let dxy = downhill_pos - chunk_pos;
451 let spline_derivative = spline_derivative.map(|e| e as f64);
458 Vec3::new(dxy - spline_derivative, spline_derivative, chunk_pos)
459}
460
461pub fn quadratic_nearest_point(
468 spline: &Vec3<Vec2<f64>>,
469 point: Vec2<f64>,
470 _line: Vec2<Vec2<f64>>, ) -> Option<(f64, Vec2<f64>, f64)> {
472 let a = spline.z.x;
518 let b = spline.y.x;
519 let c = spline.x.x;
520 let d = point.x;
521 let e = spline.z.y;
522 let f = spline.y.y;
523 let g = spline.x.y;
524 let h = point.y;
525 let a_ = (c * c + g * g) * 2.0;
540 let b_ = (b * c + g * f) * 3.0;
541 let a_d = a - d;
542 let e_h = e - h;
543 let c_ = a_d * c * 2.0 + b * b + e_h * g * 2.0 + f * f;
544 let d_ = a_d * b + e_h * f;
545 let roots = find_roots_cubic(a_, b_, c_, d_);
546 let roots = roots.as_ref();
547
548 let min_root = roots
549 .iter()
550 .copied()
551 .map(|root| {
552 let river_point = spline.x * root * root + spline.y * root + spline.z;
553 if root > 0.0 && root < 1.0 {
554 (root, river_point)
555 } else {
556 let root = root.clamped(0.0, 1.0);
557 let river_point = spline.x * root * root + spline.y * root + spline.z;
558 (root, river_point)
559 }
560 })
561 .map(|(root, river_point)| {
562 let river_distance = river_point.distance_squared(point);
563 (root, river_point, river_distance)
564 })
565 .min_by(|&(ap, _, a), &(bp, _, b)| {
568 (a, !(0.0..=1.0).contains(&ap), ap)
569 .partial_cmp(&(b, !(0.0..=1.0).contains(&bp), bp))
570 .unwrap()
571 });
572 min_root
573}