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:
2026-03-21 17:31:39 +00:00
parent 776d3caefc
commit 7fe33d4b2d
19 changed files with 159 additions and 45 deletions
+14 -5
View File
@@ -1,3 +1,4 @@
pub mod magic_numbers;
use bevy::prelude::*;
use bevy_platform::collections::HashSet;
use bevy_platform::sync::Mutex;
@@ -10,6 +11,10 @@ use std::hash::{Hash, Hasher};
use crate::{
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::{
tiles::{FixtureTileData, TileMap},
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
@@ -34,7 +39,7 @@ pub fn generate_chunk_forrestry(
events.par_read().for_each(|event| {
let floor_positions = &event.floor_tiles;
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();
SEED.hash(&mut hasher);
@@ -58,9 +63,10 @@ pub fn generate_chunk_forrestry(
.all(|&tree_pos| above_pos.distance(tree_pos) > min_distance);
// 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
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 {
let trunk_pos =
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
let base_leaf_radius = 2.25;
let base_leaf_radius = TREE_LEAF_BASE_RADIUS;
let leaf_center =
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 z_f = z as f32;
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 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;
+16 -10
View File
@@ -1,3 +1,4 @@
pub mod magic_numbers;
use bevy::prelude::*;
use bevy::tasks::AsyncComputeTaskPool;
use bevy_platform::time::Instant;
@@ -9,6 +10,11 @@ use crate::entities::item::drop_table::DropTableRegistry;
use crate::{
config::TileRegistry,
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::{
tiles::{ChunkData, FloorTileData, TileMap},
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
@@ -55,14 +61,14 @@ pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
let noise = Perlin::new(SEED);
let mut noise_value = 0.0;
let mut amplitude = 1.0;
let mut frequency = 0.008;
let mut frequency = TERRAIN_BASE_FREQUENCY;
for _ in 0..6 {
noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude;
amplitude *= 0.6;
frequency *= 1.8;
amplitude *= TERRAIN_AMPLITUDE_PERSISTENCE;
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.
@@ -99,13 +105,13 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
let surface_height =
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round();
if z < -5 {
if z < CAVE_DEPTH_THRESHOLD {
let cave_value = cave_noise.get([
world_x as f64 * 0.05,
world_y as f64 * 0.05,
z as f64 * 0.05,
world_x as f64 * CAVE_NOISE_FREQUENCY,
world_y as f64 * CAVE_NOISE_FREQUENCY,
z as f64 * CAVE_NOISE_FREQUENCY,
]);
if cave_value < -0.75 {
if cave_value < CAVE_VOID_THRESHOLD {
let tile = registry.floor("air");
tile_updates.push((
pos_ivec,
@@ -128,7 +134,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.can_stand_on,
tile.astar_weight,
);
} else if cave_value < 0.8 {
} else if cave_value < CAVE_ROCK_THRESHOLD {
let tile = registry.floor("rock");
tile_updates.push((
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;
+6 -3
View File
@@ -17,6 +17,7 @@ use bevy::prelude::*;
use crate::constants::ITILE_SIZE;
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).
/// Terrain generation uses -Z_BELOW..=Z_ABOVE (inclusive at both ends).
@@ -216,10 +217,10 @@ impl ChunkData {
#[inline]
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 {
return 100;
return ASTAR_DEFAULT_WEIGHT;
}
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);
self.astar_weights[idx]
@@ -273,7 +274,9 @@ impl ChunkData {
self.stand_in_fixture.fill(0);
self.stand_on_fixture.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);
}
}
+8
View File
@@ -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
View File
@@ -1,5 +1,6 @@
pub mod benchmark;
pub mod chunk_data;
pub mod magic_numbers;
pub mod rendering;
pub mod tile_changed;
pub mod tilemap;
+2 -1
View File
@@ -35,6 +35,7 @@ use super::chunk_data::ChunkData;
use crate::constants::ITILE_SIZE;
use crate::entities::item::drop_table::DropTable;
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.
#[derive(Clone, Debug)]
@@ -296,7 +297,7 @@ impl TileMap {
self.floor_tiles
.get(&world_pos)
.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.
+4 -2
View File
@@ -9,6 +9,7 @@ use crate::constants::{ITILE_SIZE, TILE_PIXELS, TILE_SIZE};
use crate::game::ZIndex;
use crate::world::chunks::{CHUNK_SIZE, CHUNK_SIZE_TILE, Z_BELOW, Z_TOTAL};
use crate::world::textures::TilemapTileset;
use crate::world::tiles::magic_numbers::{BYTES_PER_MB, Z_CHANGE_HISTORY_LEN};
use crate::world::tiles::{FloorTileData, TileMap};
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 {
// MAGIC u8::MAX + 1; z_index stored as u8 in layer key so this is out-of-range
if z_index >= 256 {
return false;
}
@@ -386,7 +388,7 @@ pub fn populate_tilemap_chunk_data(
let elapsed = now.elapsed().as_secs_f64() * 1000.0;
bench.last_z_change_populate_ms = 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);
}
}
@@ -431,7 +433,7 @@ pub fn populate_tilemap_chunk_data(
bench.dirty_keys_last = processed;
bench.dirty_keys_total += processed;
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(