refactor: split magic_numbers into per-module files
Extracted all doc-commented magic numbers into module-local magic_numbers.rs
files, co-located with the module that owns them. No logic changes.
- entities/shared_systems/magic_numbers: pathfinding + dig constants
- world/generation/terrain/magic_numbers: terrain + cave constants
- world/generation/forestry/magic_numbers: tree constants
- world/tiles/magic_numbers: A*, benchmark, memory constants
- entities/item/magic_numbers: item sprite z-offset
- Removed src/magic_numbers.rs (central file deleted)
Constants imported as `crate::{module}::magic_numbers::*` from each
consumer. src/constants.rs retains only structural constants (PIXEL_RATIO,
TILE_SIZE, SEED, pathfinding tier thresholds).
This commit is contained in:
@@ -7,8 +7,6 @@ pub const SEED: u32 = 420;
|
|||||||
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
|
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
|
||||||
pub const PATHFINDER_MAX_NODES: usize = 15000;
|
pub const PATHFINDER_MAX_NODES: usize = 15000;
|
||||||
pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 256;
|
pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 256;
|
||||||
|
|
||||||
// Hierarchical pathfinding thresholds
|
|
||||||
// Tier 1: Same/adjacent chunk -> sync A* (fast, ~87µs)
|
// Tier 1: Same/adjacent chunk -> sync A* (fast, ~87µs)
|
||||||
// Tier 2: 2-4 chunks away -> Provisional + full path via queue
|
// Tier 2: 2-4 chunks away -> Provisional + full path via queue
|
||||||
// Tier 3: >4 chunks away -> Hierarchical chunk-path + async segmented A*
|
// Tier 3: >4 chunks away -> Hierarchical chunk-path + async segmented A*
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ impl DropTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MAGIC Splitmix64-style hash of world SEED + tile position.
|
||||||
|
// Constants are from the splitmix64 finaliser (Sebastiano Vigna, 2018):
|
||||||
|
// 0x9e3779b97f4a7c15 — fractional bits of the golden ratio (Fibonacci hashing)
|
||||||
|
// 0x6c62272e07bb0142 — high-bit-density prime for y-axis mixing
|
||||||
|
// 0x94d049bb133111eb — splitmix64 primary multiply constant
|
||||||
|
// 0xbf58476d1ce4e5b9 — splitmix64 secondary multiply constant
|
||||||
pub fn dig_rng(pos: IVec3) -> u32 {
|
pub fn dig_rng(pos: IVec3) -> u32 {
|
||||||
let mut h = (SEED as u64)
|
let mut h = (SEED as u64)
|
||||||
.wrapping_mul(0x9e3779b97f4a7c15)
|
.wrapping_mul(0x9e3779b97f4a7c15)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Items
|
||||||
|
/// Z offset applied to item sprites to prevent z-fighting with the floor tile below.
|
||||||
|
pub const ITEM_Z_FIGHTING_OFFSET: f32 = 0.1;
|
||||||
@@ -2,6 +2,7 @@ pub mod container;
|
|||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod decorations;
|
pub mod decorations;
|
||||||
pub mod drop_table;
|
pub mod drop_table;
|
||||||
|
pub mod magic_numbers;
|
||||||
pub mod material;
|
pub mod material;
|
||||||
pub mod perishable;
|
pub mod perishable;
|
||||||
pub mod prefabs;
|
pub mod prefabs;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::entities::item::magic_numbers::ITEM_Z_FIGHTING_OFFSET;
|
||||||
use crate::entities::item::{
|
use crate::entities::item::{
|
||||||
Item, ItemBundle, ItemDecorations, ItemRotationState, ItemType, Material, Quality,
|
Item, ItemBundle, ItemDecorations, ItemRotationState, ItemType, Material, Quality,
|
||||||
};
|
};
|
||||||
@@ -33,7 +34,9 @@ pub fn spawn_prefab(
|
|||||||
item_type: ItemType::Food(crate::entities::item::FoodType::Meat),
|
item_type: ItemType::Food(crate::entities::item::FoodType::Meat),
|
||||||
base_material: Material::NA,
|
base_material: Material::NA,
|
||||||
decorations: ItemDecorations::new(),
|
decorations: ItemDecorations::new(),
|
||||||
transform: Transform::from_translation(position - Vec3::new(0.0, 0.0, 0.1)),
|
transform: Transform::from_translation(
|
||||||
|
position - Vec3::new(0.0, 0.0, ITEM_Z_FIGHTING_OFFSET),
|
||||||
|
),
|
||||||
visibility: Visibility::Visible,
|
visibility: Visibility::Visible,
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("raw_meat.png"),
|
image: asset_server.load("raw_meat.png"),
|
||||||
@@ -60,7 +63,9 @@ pub fn spawn_prefab(
|
|||||||
item_type: ItemType::Coin,
|
item_type: ItemType::Coin,
|
||||||
base_material: Material::Metal(crate::entities::item::MetalType::Copper),
|
base_material: Material::Metal(crate::entities::item::MetalType::Copper),
|
||||||
decorations: ItemDecorations::new(),
|
decorations: ItemDecorations::new(),
|
||||||
transform: Transform::from_translation(position - Vec3::new(0.0, 0.0, 0.1)),
|
transform: Transform::from_translation(
|
||||||
|
position - Vec3::new(0.0, 0.0, ITEM_Z_FIGHTING_OFFSET),
|
||||||
|
),
|
||||||
visibility: Visibility::Visible,
|
visibility: Visibility::Visible,
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("coin.png"),
|
image: asset_server.load("coin.png"),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub enum ItemType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ItemType {
|
impl ItemType {
|
||||||
// Base volume for this item type (used for weight calculation)
|
/// Base volume in cubic decimetres, used with material density for weight.
|
||||||
pub fn base_volume(&self) -> f32 {
|
pub fn base_volume(&self) -> f32 {
|
||||||
match self {
|
match self {
|
||||||
ItemType::Weapon(weapon) => match weapon {
|
ItemType::Weapon(weapon) => match weapon {
|
||||||
@@ -46,23 +46,23 @@ impl ItemType {
|
|||||||
ToolType::Shovel => 1.5,
|
ToolType::Shovel => 1.5,
|
||||||
ToolType::Axe => 1.75,
|
ToolType::Axe => 1.75,
|
||||||
ToolType::Hammer => 1.0,
|
ToolType::Hammer => 1.0,
|
||||||
ToolType::Anvil => 50.0, // Heavy!
|
ToolType::Anvil => 50.0, // MAGIC ~50kg — intentionally immovable in practice
|
||||||
ToolType::Furnace => 100.0, // Very heavy!
|
ToolType::Furnace => 100.0, // MAGIC ~100kg — structure, not a carried item
|
||||||
},
|
},
|
||||||
ItemType::Food(_) => 0.2,
|
ItemType::Food(_) => 0.2,
|
||||||
ItemType::Drink(_) => 0.3,
|
ItemType::Drink(_) => 0.3,
|
||||||
ItemType::Medicine => 0.1,
|
ItemType::Medicine => 0.1,
|
||||||
ItemType::RawMaterial => 0.5,
|
ItemType::RawMaterial => 0.5,
|
||||||
ItemType::Gem => 0.1,
|
ItemType::Gem => 0.1,
|
||||||
ItemType::Coin => 0.01,
|
ItemType::Coin => 0.01, // MAGIC ~1g coin, negligible individual weight
|
||||||
ItemType::Container => 1.0,
|
ItemType::Container => 1.0, // MAGIC empty container only, not contents
|
||||||
ItemType::Book => 0.5,
|
ItemType::Book => 0.5,
|
||||||
ItemType::Toy => 0.2,
|
ItemType::Toy => 0.2,
|
||||||
ItemType::Decoration => 0.3,
|
ItemType::Decoration => 0.3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base value for this item type
|
/// Base value in abstract currency units, before quality and material modifiers.
|
||||||
pub fn base_value(&self) -> u32 {
|
pub fn base_value(&self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
ItemType::Weapon(_) => 50,
|
ItemType::Weapon(_) => 50,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::config::GameConfig;
|
|||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
use crate::entities::shared_systems::digging::Digger;
|
use crate::entities::shared_systems::digging::Digger;
|
||||||
|
use crate::entities::shared_systems::magic_numbers::DEFAULT_DIG_INTERVAL_SECS;
|
||||||
use crate::game::SpawnDelay;
|
use crate::game::SpawnDelay;
|
||||||
use crate::world::VisibleGameEntity;
|
use crate::world::VisibleGameEntity;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
@@ -37,7 +38,7 @@ impl Rabbit {
|
|||||||
},
|
},
|
||||||
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||||
visibility: Visibility::Hidden,
|
visibility: Visibility::Hidden,
|
||||||
digger: Digger::new(5.0),
|
digger: Digger::new(DEFAULT_DIG_INTERVAL_SECS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Pathfinding + digging
|
||||||
|
/// Number of future path steps validated each tick before an entity moves.
|
||||||
|
pub const PATHFINDER_VALIDATE_STEPS: usize = 3;
|
||||||
|
/// Ticks between path validation checks per entity.
|
||||||
|
pub const PATHFINDER_VALIDATION_COOLDOWN: u8 = 10;
|
||||||
|
/// Maximum path steps looked ahead when invalidating paths on tile change.
|
||||||
|
pub const PATHFINDER_DIRTY_LOOKAHEAD: usize = 8;
|
||||||
|
/// Divisor in excuse-me threshold: (walk_speed * tile_weight) / WALK_SPEED_DIVISOR.
|
||||||
|
pub const WALK_SPEED_DIVISOR: i32 = 50;
|
||||||
|
/// Dot product above which entities are considered moving in the same direction (convoy). cos(45°) ≈ 0.707.
|
||||||
|
pub const CONVOY_DOT_THRESHOLD: f32 = 0.7;
|
||||||
|
/// Dot product below which entities are considered head-on.
|
||||||
|
pub const HEAD_ON_DOT_THRESHOLD: f32 = -0.7;
|
||||||
|
/// Directional component threshold for the E/S yield rule in 2-wide corridors.
|
||||||
|
pub const ES_DIRECTION_THRESHOLD: f32 = 0.3;
|
||||||
|
/// Tile occupancy count above which an entity ignores collision (crowded tile bypass).
|
||||||
|
pub const OCCUPANCY_CROWD_THRESHOLD: u8 = 3;
|
||||||
|
/// Default dig interval for Digger entities in seconds.
|
||||||
|
pub const DEFAULT_DIG_INTERVAL_SECS: f32 = 5.0;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod digging;
|
pub mod digging;
|
||||||
|
pub mod magic_numbers;
|
||||||
pub mod occupancy;
|
pub mod occupancy;
|
||||||
pub mod pathfinding;
|
pub mod pathfinding;
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ use crate::constants::{
|
|||||||
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
||||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
||||||
};
|
};
|
||||||
|
use crate::entities::shared_systems::magic_numbers::{
|
||||||
|
CONVOY_DOT_THRESHOLD, ES_DIRECTION_THRESHOLD, HEAD_ON_DOT_THRESHOLD, OCCUPANCY_CROWD_THRESHOLD,
|
||||||
|
PATHFINDER_DIRTY_LOOKAHEAD, PATHFINDER_VALIDATE_STEPS, PATHFINDER_VALIDATION_COOLDOWN,
|
||||||
|
WALK_SPEED_DIVISOR,
|
||||||
|
};
|
||||||
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
||||||
use crate::world::tiles::tile_changed::{PathfindingDirtyChunks, TileChangedEvent};
|
use crate::world::tiles::tile_changed::{PathfindingDirtyChunks, TileChangedEvent};
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
@@ -272,7 +277,7 @@ pub fn invalidate_paths_on_tile_change(
|
|||||||
let Some(ref path) = ambulatory.current_path else {
|
let Some(ref path) = ambulatory.current_path else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let check_end = (ambulatory.path_index + 8).min(path.len());
|
let check_end = (ambulatory.path_index + PATHFINDER_DIRTY_LOOKAHEAD).min(path.len());
|
||||||
let affected = path[ambulatory.path_index..check_end]
|
let affected = path[ambulatory.path_index..check_end]
|
||||||
.iter()
|
.iter()
|
||||||
.any(|p| dirty.chunks.contains(&world_to_chunk(p.as_ivec3())));
|
.any(|p| dirty.chunks.contains(&world_to_chunk(p.as_ivec3())));
|
||||||
@@ -629,17 +634,23 @@ pub fn movement(
|
|||||||
if ambulatory.validation_cooldown > 0 {
|
if ambulatory.validation_cooldown > 0 {
|
||||||
ambulatory.validation_cooldown -= 1;
|
ambulatory.validation_cooldown -= 1;
|
||||||
} else if let Some(path) = &ambulatory.current_path {
|
} else if let Some(path) = &ambulatory.current_path {
|
||||||
if !validate_next_steps(&tilemap, path, ambulatory.path_index, 3) {
|
if !validate_next_steps(
|
||||||
|
&tilemap,
|
||||||
|
path,
|
||||||
|
ambulatory.path_index,
|
||||||
|
PATHFINDER_VALIDATE_STEPS,
|
||||||
|
) {
|
||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
ambulatory.validation_cooldown = 10;
|
ambulatory.validation_cooldown = PATHFINDER_VALIDATION_COOLDOWN;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ambulatory.validation_cooldown = 10;
|
ambulatory.validation_cooldown = PATHFINDER_VALIDATION_COOLDOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ambulatory.walk_speed > 0. {
|
if ambulatory.walk_speed > 0. {
|
||||||
let tile_weight = get_tile_weight(&tilemap, current_pos.as_ivec3());
|
let tile_weight = get_tile_weight(&tilemap, current_pos.as_ivec3());
|
||||||
let threshold = (ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32;
|
let threshold =
|
||||||
|
(ambulatory.walk_speed as i32 * tile_weight as i32 / WALK_SPEED_DIVISOR) as u32;
|
||||||
|
|
||||||
if ambulatory.step_recovery <= threshold {
|
if ambulatory.step_recovery <= threshold {
|
||||||
ambulatory.step_recovery += 1;
|
ambulatory.step_recovery += 1;
|
||||||
@@ -656,7 +667,7 @@ pub fn movement(
|
|||||||
|
|
||||||
let current_count = occupancy.count_at(transform.translation);
|
let current_count = occupancy.count_at(transform.translation);
|
||||||
let next_count = occupancy.count_at(next_point);
|
let next_count = occupancy.count_at(next_point);
|
||||||
let current_crowded = current_count > 3;
|
let current_crowded = current_count > OCCUPANCY_CROWD_THRESHOLD;
|
||||||
let occupied = !current_crowded && next_count > current_count;
|
let occupied = !current_crowded && next_count > current_count;
|
||||||
|
|
||||||
let actual_move: Vec3;
|
let actual_move: Vec3;
|
||||||
@@ -671,13 +682,16 @@ pub fn movement(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Case 1: Convoy — same direction, treat as unoccupied
|
// Case 1: Convoy — same direction, treat as unoccupied
|
||||||
let convoy =
|
let convoy = occupied
|
||||||
occupied && their_dir != Vec2::ZERO && their_dir.dot(our_dir) > 0.7;
|
&& their_dir != Vec2::ZERO
|
||||||
|
&& their_dir.dot(our_dir) > CONVOY_DOT_THRESHOLD;
|
||||||
|
|
||||||
// Case 2: Head-on (directly opposite directions)
|
// Case 2: Head-on (directly opposite directions)
|
||||||
let head_on = their_dir != Vec2::ZERO && their_dir.dot(our_dir) < -0.7;
|
let head_on =
|
||||||
|
their_dir != Vec2::ZERO && their_dir.dot(our_dir) < HEAD_ON_DOT_THRESHOLD;
|
||||||
|
|
||||||
let we_are_es = our_dir.x > 0.3 || our_dir.y < -0.3;
|
let we_are_es =
|
||||||
|
our_dir.x > ES_DIRECTION_THRESHOLD || our_dir.y < -ES_DIRECTION_THRESHOLD;
|
||||||
|
|
||||||
if !occupied || convoy {
|
if !occupied || convoy {
|
||||||
// Normal movement
|
// Normal movement
|
||||||
@@ -743,7 +757,8 @@ pub fn movement(
|
|||||||
let tile_weight =
|
let tile_weight =
|
||||||
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
||||||
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
(ambulatory.walk_speed as i32 * tile_weight as i32
|
||||||
|
/ WALK_SPEED_DIVISOR) as u32
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
@@ -812,7 +827,8 @@ pub fn movement(
|
|||||||
let tile_weight =
|
let tile_weight =
|
||||||
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
||||||
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
(ambulatory.walk_speed as i32 * tile_weight as i32
|
||||||
|
/ WALK_SPEED_DIVISOR) as u32
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
@@ -827,7 +843,8 @@ pub fn movement(
|
|||||||
let tile_weight =
|
let tile_weight =
|
||||||
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
||||||
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
(ambulatory.walk_speed as i32 * tile_weight as i32 / WALK_SPEED_DIVISOR)
|
||||||
|
as u32
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
@@ -1056,6 +1073,8 @@ fn directional_chunk_waypoint(
|
|||||||
// goal varies per entity (each has a different random wander target).
|
// goal varies per entity (each has a different random wander target).
|
||||||
// Together they produce unique spread values for entities even when
|
// Together they produce unique spread values for entities even when
|
||||||
// heading through the same chunk, without needing to pass entity ID.
|
// heading through the same chunk, without needing to pass entity ID.
|
||||||
|
// MAGIC Small primes spread entity starting positions across waypoint edge tiles.
|
||||||
|
// Different values per axis prevent aliasing when positions are on a regular grid.
|
||||||
let entropy = (cur_tile.x.unsigned_abs() as usize)
|
let entropy = (cur_tile.x.unsigned_abs() as usize)
|
||||||
.wrapping_mul(1619)
|
.wrapping_mul(1619)
|
||||||
.wrapping_add((cur_tile.y.unsigned_abs() as usize).wrapping_mul(31337))
|
.wrapping_add((cur_tile.y.unsigned_abs() as usize).wrapping_mul(31337))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod magic_numbers;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_platform::collections::HashSet;
|
use bevy_platform::collections::HashSet;
|
||||||
use bevy_platform::sync::Mutex;
|
use bevy_platform::sync::Mutex;
|
||||||
@@ -10,6 +11,10 @@ use std::hash::{Hash, Hasher};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{SEED, TILE_SIZE},
|
constants::{SEED, TILE_SIZE},
|
||||||
|
world::generation::forestry::magic_numbers::{
|
||||||
|
TREE_LEAF_BASE_RADIUS, TREE_LEAF_RADIUS_OFFSET, TREE_LEAF_RADIUS_VARIATION,
|
||||||
|
TREE_MIN_DISTANCE_TILES, TREE_SPAWN_CHANCE, TREE_TRUNK_EXTRA_HEIGHT, TREE_TRUNK_MIN_HEIGHT,
|
||||||
|
},
|
||||||
world::{
|
world::{
|
||||||
tiles::{FixtureTileData, TileMap},
|
tiles::{FixtureTileData, TileMap},
|
||||||
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
|
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
|
||||||
@@ -34,7 +39,7 @@ pub fn generate_chunk_forrestry(
|
|||||||
events.par_read().for_each(|event| {
|
events.par_read().for_each(|event| {
|
||||||
let floor_positions = &event.floor_tiles;
|
let floor_positions = &event.floor_tiles;
|
||||||
let mut tree_positions: Vec<Vec3> = Vec::new();
|
let mut tree_positions: Vec<Vec3> = Vec::new();
|
||||||
let min_distance = 7.0 * TILE_SIZE;
|
let min_distance = TREE_MIN_DISTANCE_TILES * TILE_SIZE;
|
||||||
|
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
SEED.hash(&mut hasher);
|
SEED.hash(&mut hasher);
|
||||||
@@ -58,9 +63,10 @@ pub fn generate_chunk_forrestry(
|
|||||||
.all(|&tree_pos| above_pos.distance(tree_pos) > min_distance);
|
.all(|&tree_pos| above_pos.distance(tree_pos) > min_distance);
|
||||||
|
|
||||||
// 1 in 100 chance if far enough from other trees
|
// 1 in 100 chance if far enough from other trees
|
||||||
if is_far_enough && rng.random::<u32>() % 100 == 0 {
|
if is_far_enough && rng.random::<u32>() % TREE_SPAWN_CHANCE == 0 {
|
||||||
// Generate trunk
|
// Generate trunk
|
||||||
let trunk_height = 4 + rng.random::<u32>() % 5;
|
let trunk_height = TREE_TRUNK_MIN_HEIGHT
|
||||||
|
+ rng.random::<u32>() % TREE_TRUNK_EXTRA_HEIGHT;
|
||||||
for i in 0..trunk_height {
|
for i in 0..trunk_height {
|
||||||
let trunk_pos =
|
let trunk_pos =
|
||||||
above_pos + Vec3::new(0.0, 0.0, i as f32 * TILE_SIZE);
|
above_pos + Vec3::new(0.0, 0.0, i as f32 * TILE_SIZE);
|
||||||
@@ -110,7 +116,7 @@ pub fn generate_chunk_forrestry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate leaves - 3D spherical canopy with random shape
|
// Generate leaves - 3D spherical canopy with random shape
|
||||||
let base_leaf_radius = 2.25;
|
let base_leaf_radius = TREE_LEAF_BASE_RADIUS;
|
||||||
let leaf_center =
|
let leaf_center =
|
||||||
above_pos + Vec3::new(0.0, 0.0, trunk_height as f32 * TILE_SIZE);
|
above_pos + Vec3::new(0.0, 0.0, trunk_height as f32 * TILE_SIZE);
|
||||||
|
|
||||||
@@ -136,7 +142,10 @@ pub fn generate_chunk_forrestry(
|
|||||||
let y_f = y as f32;
|
let y_f = y as f32;
|
||||||
let z_f = z as f32;
|
let z_f = z as f32;
|
||||||
let radius = base_leaf_radius
|
let radius = base_leaf_radius
|
||||||
* (1.0 + (rng.random::<f32>() * 0.35 - 0.1));
|
* (1.0
|
||||||
|
+ (rng.random::<f32>()
|
||||||
|
* TREE_LEAF_RADIUS_VARIATION
|
||||||
|
- TREE_LEAF_RADIUS_OFFSET));
|
||||||
|
|
||||||
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
|
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
|
||||||
if let Some(texture_id) = texture_ids.refs.get(&500005)
|
if let Some(texture_id) = texture_ids.refs.get(&500005)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Forestry
|
||||||
|
/// Minimum distance between tree trunks in tile units. Prevents overlapping canopies.
|
||||||
|
pub const TREE_MIN_DISTANCE_TILES: f32 = 7.0;
|
||||||
|
/// 1-in-N chance per eligible grass tile of spawning a tree.
|
||||||
|
pub const TREE_SPAWN_CHANCE: u32 = 100;
|
||||||
|
/// Minimum trunk height in segments (inclusive).
|
||||||
|
pub const TREE_TRUNK_MIN_HEIGHT: u32 = 4;
|
||||||
|
/// Random additional trunk segments added on top of minimum.
|
||||||
|
pub const TREE_TRUNK_EXTRA_HEIGHT: u32 = 5;
|
||||||
|
/// Base radius of the spherical leaf canopy in tile units.
|
||||||
|
pub const TREE_LEAF_BASE_RADIUS: f32 = 2.25;
|
||||||
|
/// Random per-leaf radius scale variation: radius * (1.0 + rand * RANGE - OFFSET).
|
||||||
|
pub const TREE_LEAF_RADIUS_VARIATION: f32 = 0.35;
|
||||||
|
pub const TREE_LEAF_RADIUS_OFFSET: f32 = 0.1;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod magic_numbers;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::tasks::AsyncComputeTaskPool;
|
use bevy::tasks::AsyncComputeTaskPool;
|
||||||
use bevy_platform::time::Instant;
|
use bevy_platform::time::Instant;
|
||||||
@@ -9,6 +10,11 @@ use crate::entities::item::drop_table::DropTableRegistry;
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::TileRegistry,
|
config::TileRegistry,
|
||||||
constants::{SEED, TILE_SIZE},
|
constants::{SEED, TILE_SIZE},
|
||||||
|
world::generation::terrain::magic_numbers::{
|
||||||
|
CAVE_DEPTH_THRESHOLD, CAVE_NOISE_FREQUENCY, CAVE_ROCK_THRESHOLD, CAVE_VOID_THRESHOLD,
|
||||||
|
TERRAIN_AMPLITUDE_PERSISTENCE, TERRAIN_BASE_FREQUENCY,
|
||||||
|
TERRAIN_FREQUENCY_LACUNARITY, TERRAIN_HEIGHT_SCALE,
|
||||||
|
},
|
||||||
world::{
|
world::{
|
||||||
tiles::{ChunkData, FloorTileData, TileMap},
|
tiles::{ChunkData, FloorTileData, TileMap},
|
||||||
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
|
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
|
||||||
@@ -55,14 +61,14 @@ pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
|
|||||||
let noise = Perlin::new(SEED);
|
let noise = Perlin::new(SEED);
|
||||||
let mut noise_value = 0.0;
|
let mut noise_value = 0.0;
|
||||||
let mut amplitude = 1.0;
|
let mut amplitude = 1.0;
|
||||||
let mut frequency = 0.008;
|
let mut frequency = TERRAIN_BASE_FREQUENCY;
|
||||||
|
|
||||||
for _ in 0..6 {
|
for _ in 0..6 {
|
||||||
noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude;
|
noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude;
|
||||||
amplitude *= 0.6;
|
amplitude *= TERRAIN_AMPLITUDE_PERSISTENCE;
|
||||||
frequency *= 1.8;
|
frequency *= TERRAIN_FREQUENCY_LACUNARITY;
|
||||||
}
|
}
|
||||||
(noise_value * 2.5) as f32
|
(noise_value * TERRAIN_HEIGHT_SCALE as f64) as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Async terrain generation - runs on AsyncComputeTaskPool.
|
/// Async terrain generation - runs on AsyncComputeTaskPool.
|
||||||
@@ -99,13 +105,13 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
let surface_height =
|
let surface_height =
|
||||||
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round();
|
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round();
|
||||||
|
|
||||||
if z < -5 {
|
if z < CAVE_DEPTH_THRESHOLD {
|
||||||
let cave_value = cave_noise.get([
|
let cave_value = cave_noise.get([
|
||||||
world_x as f64 * 0.05,
|
world_x as f64 * CAVE_NOISE_FREQUENCY,
|
||||||
world_y as f64 * 0.05,
|
world_y as f64 * CAVE_NOISE_FREQUENCY,
|
||||||
z as f64 * 0.05,
|
z as f64 * CAVE_NOISE_FREQUENCY,
|
||||||
]);
|
]);
|
||||||
if cave_value < -0.75 {
|
if cave_value < CAVE_VOID_THRESHOLD {
|
||||||
let tile = registry.floor("air");
|
let tile = registry.floor("air");
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
@@ -128,7 +134,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.can_stand_on,
|
tile.can_stand_on,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
);
|
);
|
||||||
} else if cave_value < 0.8 {
|
} else if cave_value < CAVE_ROCK_THRESHOLD {
|
||||||
let tile = registry.floor("rock");
|
let tile = registry.floor("rock");
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Terrain generation
|
||||||
|
/// Noise frequency for the coarsest octave of surface terrain height.
|
||||||
|
pub const TERRAIN_BASE_FREQUENCY: f64 = 0.008;
|
||||||
|
/// Amplitude multiplier per noise octave (persistence). 0.6 = moderate detail.
|
||||||
|
pub const TERRAIN_AMPLITUDE_PERSISTENCE: f64 = 0.6;
|
||||||
|
/// Frequency multiplier per noise octave (lacunarity).
|
||||||
|
pub const TERRAIN_FREQUENCY_LACUNARITY: f64 = 1.8;
|
||||||
|
/// Scale applied to summed octave noise to set terrain height range in tile units.
|
||||||
|
pub const TERRAIN_HEIGHT_SCALE: f32 = 2.5;
|
||||||
|
/// Noise frequency for cave generation (applied to all three axes).
|
||||||
|
pub const CAVE_NOISE_FREQUENCY: f64 = 0.05;
|
||||||
|
/// Cave noise value below which a tile becomes open air.
|
||||||
|
pub const CAVE_VOID_THRESHOLD: f64 = -0.75;
|
||||||
|
/// Cave noise value below which a tile becomes dirt rather than rock.
|
||||||
|
pub const CAVE_ROCK_THRESHOLD: f64 = 0.8;
|
||||||
|
/// Tile z index below which cave generation logic applies instead of surface rules.
|
||||||
|
pub const CAVE_DEPTH_THRESHOLD: isize = -5;
|
||||||
@@ -17,6 +17,7 @@ use bevy::prelude::*;
|
|||||||
|
|
||||||
use crate::constants::ITILE_SIZE;
|
use crate::constants::ITILE_SIZE;
|
||||||
use crate::world::chunks::{CHUNK_SIZE, Z_ABOVE, Z_BELOW};
|
use crate::world::chunks::{CHUNK_SIZE, Z_ABOVE, Z_BELOW};
|
||||||
|
use crate::world::tiles::magic_numbers::ASTAR_DEFAULT_WEIGHT;
|
||||||
|
|
||||||
/// Number of z-levels in a chunk (Z_BELOW + Z_ABOVE + 1 for inclusive range).
|
/// Number of z-levels in a chunk (Z_BELOW + Z_ABOVE + 1 for inclusive range).
|
||||||
/// Terrain generation uses -Z_BELOW..=Z_ABOVE (inclusive at both ends).
|
/// Terrain generation uses -Z_BELOW..=Z_ABOVE (inclusive at both ends).
|
||||||
@@ -216,10 +217,10 @@ impl ChunkData {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_astar_weight(&self, local_x: i32, local_y: i32, z: i32) -> u8 {
|
pub fn get_astar_weight(&self, local_x: i32, local_y: i32, z: i32) -> u8 {
|
||||||
if local_x < 0 || local_x >= CHUNK_SIZE || local_y < 0 || local_y >= CHUNK_SIZE {
|
if local_x < 0 || local_x >= CHUNK_SIZE || local_y < 0 || local_y >= CHUNK_SIZE {
|
||||||
return 100;
|
return ASTAR_DEFAULT_WEIGHT;
|
||||||
}
|
}
|
||||||
if z < -(Z_BELOW as i32) || z > (Z_ABOVE as i32) {
|
if z < -(Z_BELOW as i32) || z > (Z_ABOVE as i32) {
|
||||||
return 100;
|
return ASTAR_DEFAULT_WEIGHT;
|
||||||
}
|
}
|
||||||
let idx = Self::pos_to_index(local_x, local_y, z);
|
let idx = Self::pos_to_index(local_x, local_y, z);
|
||||||
self.astar_weights[idx]
|
self.astar_weights[idx]
|
||||||
@@ -273,7 +274,9 @@ impl ChunkData {
|
|||||||
self.stand_in_fixture.fill(0);
|
self.stand_in_fixture.fill(0);
|
||||||
self.stand_on_fixture.fill(0);
|
self.stand_on_fixture.fill(0);
|
||||||
self.tile_ids.fill(0);
|
self.tile_ids.fill(0);
|
||||||
self.astar_weights.fill(100);
|
// MAGIC ASTAR_DEFAULT_WEIGHT matches normal grass/traversable tile weight — cleared
|
||||||
|
// chunks present as passable rather than impassable walls to pathfinding.
|
||||||
|
self.astar_weights.fill(ASTAR_DEFAULT_WEIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Tiles
|
||||||
|
/// Default A* weight returned for out-of-bounds or unloaded positions.
|
||||||
|
/// Matches normal traversable tile weight so pathfinding degrades gracefully.
|
||||||
|
pub const ASTAR_DEFAULT_WEIGHT: u8 = 100;
|
||||||
|
/// Rolling history length for z-change frame timing in benchmark reporting.
|
||||||
|
pub const Z_CHANGE_HISTORY_LEN: usize = 100;
|
||||||
|
/// Bytes per megabyte, for memory reporting.
|
||||||
|
pub const BYTES_PER_MB: f32 = 1_048_576.0;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod benchmark;
|
pub mod benchmark;
|
||||||
pub mod chunk_data;
|
pub mod chunk_data;
|
||||||
|
pub mod magic_numbers;
|
||||||
pub mod rendering;
|
pub mod rendering;
|
||||||
pub mod tile_changed;
|
pub mod tile_changed;
|
||||||
pub mod tilemap;
|
pub mod tilemap;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use super::chunk_data::ChunkData;
|
|||||||
use crate::constants::ITILE_SIZE;
|
use crate::constants::ITILE_SIZE;
|
||||||
use crate::entities::item::drop_table::DropTable;
|
use crate::entities::item::drop_table::DropTable;
|
||||||
use crate::world::chunks::{world_to_chunk, CHUNK_SIZE, Z_ABOVE, Z_BELOW};
|
use crate::world::chunks::{world_to_chunk, CHUNK_SIZE, Z_ABOVE, Z_BELOW};
|
||||||
|
use crate::world::tiles::magic_numbers::ASTAR_DEFAULT_WEIGHT;
|
||||||
|
|
||||||
/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple.
|
/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -296,7 +297,7 @@ impl TileMap {
|
|||||||
self.floor_tiles
|
self.floor_tiles
|
||||||
.get(&world_pos)
|
.get(&world_pos)
|
||||||
.map(|t| t.astar_weight)
|
.map(|t| t.astar_weight)
|
||||||
.unwrap_or(100)
|
.unwrap_or(ASTAR_DEFAULT_WEIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets.
|
/// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::constants::{ITILE_SIZE, TILE_PIXELS, TILE_SIZE};
|
|||||||
use crate::game::ZIndex;
|
use crate::game::ZIndex;
|
||||||
use crate::world::chunks::{CHUNK_SIZE, CHUNK_SIZE_TILE, Z_BELOW, Z_TOTAL};
|
use crate::world::chunks::{CHUNK_SIZE, CHUNK_SIZE_TILE, Z_BELOW, Z_TOTAL};
|
||||||
use crate::world::textures::TilemapTileset;
|
use crate::world::textures::TilemapTileset;
|
||||||
|
use crate::world::tiles::magic_numbers::{BYTES_PER_MB, Z_CHANGE_HISTORY_LEN};
|
||||||
use crate::world::tiles::{FloorTileData, TileMap};
|
use crate::world::tiles::{FloorTileData, TileMap};
|
||||||
|
|
||||||
pub const LAYER_FLOOR: u8 = 0;
|
pub const LAYER_FLOOR: u8 = 0;
|
||||||
@@ -139,6 +140,7 @@ fn tile_for_depth(real_index: u16, z_diff: i32) -> TileData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_tile_visible_at_z(tile_data: &FloorTileData, z_index: usize) -> bool {
|
fn is_tile_visible_at_z(tile_data: &FloorTileData, z_index: usize) -> bool {
|
||||||
|
// MAGIC u8::MAX + 1; z_index stored as u8 in layer key so this is out-of-range
|
||||||
if z_index >= 256 {
|
if z_index >= 256 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -386,7 +388,7 @@ pub fn populate_tilemap_chunk_data(
|
|||||||
let elapsed = now.elapsed().as_secs_f64() * 1000.0;
|
let elapsed = now.elapsed().as_secs_f64() * 1000.0;
|
||||||
bench.last_z_change_populate_ms = elapsed;
|
bench.last_z_change_populate_ms = elapsed;
|
||||||
bench.z_change_history_ms.push(elapsed);
|
bench.z_change_history_ms.push(elapsed);
|
||||||
if bench.z_change_history_ms.len() > 100 {
|
if bench.z_change_history_ms.len() > Z_CHANGE_HISTORY_LEN {
|
||||||
bench.z_change_history_ms.remove(0);
|
bench.z_change_history_ms.remove(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,7 +433,7 @@ pub fn populate_tilemap_chunk_data(
|
|||||||
bench.dirty_keys_last = processed;
|
bench.dirty_keys_last = processed;
|
||||||
bench.dirty_keys_total += processed;
|
bench.dirty_keys_total += processed;
|
||||||
bench.tile_data_mb = (registry.entities.len() as f64 * (CHUNK_SIZE * CHUNK_SIZE) as f64 * 4.0)
|
bench.tile_data_mb = (registry.entities.len() as f64 * (CHUNK_SIZE * CHUNK_SIZE) as f64 * 4.0)
|
||||||
/ (1024.0 * 1024.0);
|
/ BYTES_PER_MB as f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_camera_z_changed(
|
pub fn on_camera_z_changed(
|
||||||
|
|||||||
Reference in New Issue
Block a user