diff --git a/src/constants.rs b/src/constants.rs index 83a29b1..336d73a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -7,8 +7,6 @@ pub const SEED: u32 = 420; pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64; pub const PATHFINDER_MAX_NODES: usize = 15000; pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 256; - -// Hierarchical pathfinding thresholds // Tier 1: Same/adjacent chunk -> sync A* (fast, ~87µs) // Tier 2: 2-4 chunks away -> Provisional + full path via queue // Tier 3: >4 chunks away -> Hierarchical chunk-path + async segmented A* diff --git a/src/entities/item/drop_table.rs b/src/entities/item/drop_table.rs index f9e48e6..c6d5465 100644 --- a/src/entities/item/drop_table.rs +++ b/src/entities/item/drop_table.rs @@ -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 { let mut h = (SEED as u64) .wrapping_mul(0x9e3779b97f4a7c15) diff --git a/src/entities/item/magic_numbers.rs b/src/entities/item/magic_numbers.rs new file mode 100644 index 0000000..12c09f9 --- /dev/null +++ b/src/entities/item/magic_numbers.rs @@ -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; diff --git a/src/entities/item/mod.rs b/src/entities/item/mod.rs index 4d2ae5f..191a4d7 100644 --- a/src/entities/item/mod.rs +++ b/src/entities/item/mod.rs @@ -2,6 +2,7 @@ pub mod container; pub mod core; pub mod decorations; pub mod drop_table; +pub mod magic_numbers; pub mod material; pub mod perishable; pub mod prefabs; diff --git a/src/entities/item/prefabs/misc/misc_prefabs.rs b/src/entities/item/prefabs/misc/misc_prefabs.rs index f5d9706..56754ae 100644 --- a/src/entities/item/prefabs/misc/misc_prefabs.rs +++ b/src/entities/item/prefabs/misc/misc_prefabs.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; +use crate::entities::item::magic_numbers::ITEM_Z_FIGHTING_OFFSET; use crate::entities::item::{ Item, ItemBundle, ItemDecorations, ItemRotationState, ItemType, Material, Quality, }; @@ -33,7 +34,9 @@ pub fn spawn_prefab( item_type: ItemType::Food(crate::entities::item::FoodType::Meat), base_material: Material::NA, 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, sprite: Sprite { image: asset_server.load("raw_meat.png"), @@ -60,7 +63,9 @@ pub fn spawn_prefab( item_type: ItemType::Coin, base_material: Material::Metal(crate::entities::item::MetalType::Copper), 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, sprite: Sprite { image: asset_server.load("coin.png"), diff --git a/src/entities/item/types.rs b/src/entities/item/types.rs index 0551965..4f293cf 100644 --- a/src/entities/item/types.rs +++ b/src/entities/item/types.rs @@ -18,7 +18,7 @@ pub enum 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 { match self { ItemType::Weapon(weapon) => match weapon { @@ -46,23 +46,23 @@ impl ItemType { ToolType::Shovel => 1.5, ToolType::Axe => 1.75, ToolType::Hammer => 1.0, - ToolType::Anvil => 50.0, // Heavy! - ToolType::Furnace => 100.0, // Very heavy! + ToolType::Anvil => 50.0, // MAGIC ~50kg — intentionally immovable in practice + ToolType::Furnace => 100.0, // MAGIC ~100kg — structure, not a carried item }, ItemType::Food(_) => 0.2, ItemType::Drink(_) => 0.3, ItemType::Medicine => 0.1, ItemType::RawMaterial => 0.5, ItemType::Gem => 0.1, - ItemType::Coin => 0.01, - ItemType::Container => 1.0, + ItemType::Coin => 0.01, // MAGIC ~1g coin, negligible individual weight + ItemType::Container => 1.0, // MAGIC empty container only, not contents ItemType::Book => 0.5, ItemType::Toy => 0.2, 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 { match self { ItemType::Weapon(_) => 50, diff --git a/src/entities/livestock/rabbit.rs b/src/entities/livestock/rabbit.rs index d4168f3..3f6a6a4 100644 --- a/src/entities/livestock/rabbit.rs +++ b/src/entities/livestock/rabbit.rs @@ -2,6 +2,7 @@ use crate::config::GameConfig; use crate::constants::*; use crate::entities::shared_components::Ambulatory; use crate::entities::shared_systems::digging::Digger; +use crate::entities::shared_systems::magic_numbers::DEFAULT_DIG_INTERVAL_SECS; use crate::game::SpawnDelay; use crate::world::VisibleGameEntity; use bevy::prelude::*; @@ -37,7 +38,7 @@ impl Rabbit { }, transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), visibility: Visibility::Hidden, - digger: Digger::new(5.0), + digger: Digger::new(DEFAULT_DIG_INTERVAL_SECS), } } } diff --git a/src/entities/shared_systems/magic_numbers.rs b/src/entities/shared_systems/magic_numbers.rs new file mode 100644 index 0000000..cf7a094 --- /dev/null +++ b/src/entities/shared_systems/magic_numbers.rs @@ -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; diff --git a/src/entities/shared_systems/mod.rs b/src/entities/shared_systems/mod.rs index b2c0a55..eb5d2a6 100644 --- a/src/entities/shared_systems/mod.rs +++ b/src/entities/shared_systems/mod.rs @@ -1,3 +1,4 @@ pub mod digging; +pub mod magic_numbers; pub mod occupancy; pub mod pathfinding; diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs index 77ec7b6..8ec0521 100644 --- a/src/entities/shared_systems/pathfinding.rs +++ b/src/entities/shared_systems/pathfinding.rs @@ -63,6 +63,11 @@ use crate::constants::{ ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES, 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::world::tiles::tile_changed::{PathfindingDirtyChunks, TileChangedEvent}; use crate::world::tiles::TileMap; @@ -272,7 +277,7 @@ pub fn invalidate_paths_on_tile_change( let Some(ref path) = ambulatory.current_path else { 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] .iter() .any(|p| dirty.chunks.contains(&world_to_chunk(p.as_ivec3()))); @@ -629,17 +634,23 @@ pub fn movement( if ambulatory.validation_cooldown > 0 { ambulatory.validation_cooldown -= 1; } 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.validation_cooldown = 10; + ambulatory.validation_cooldown = PATHFINDER_VALIDATION_COOLDOWN; return; } - ambulatory.validation_cooldown = 10; + ambulatory.validation_cooldown = PATHFINDER_VALIDATION_COOLDOWN; } if ambulatory.walk_speed > 0. { 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 { ambulatory.step_recovery += 1; @@ -656,7 +667,7 @@ pub fn movement( let current_count = occupancy.count_at(transform.translation); 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 actual_move: Vec3; @@ -671,13 +682,16 @@ pub fn movement( }; // Case 1: Convoy — same direction, treat as unoccupied - let convoy = - occupied && their_dir != Vec2::ZERO && their_dir.dot(our_dir) > 0.7; + let convoy = occupied + && their_dir != Vec2::ZERO + && their_dir.dot(our_dir) > CONVOY_DOT_THRESHOLD; // 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 { // Normal movement @@ -743,7 +757,8 @@ pub fn movement( let tile_weight = get_tile_weight(&tilemap, transform.translation.as_ivec3()); 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 { 0 }; @@ -812,7 +827,8 @@ pub fn movement( let tile_weight = get_tile_weight(&tilemap, transform.translation.as_ivec3()); 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 { 0 }; @@ -827,7 +843,8 @@ pub fn movement( let tile_weight = get_tile_weight(&tilemap, transform.translation.as_ivec3()); 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 { 0 }; @@ -1056,6 +1073,8 @@ fn directional_chunk_waypoint( // goal varies per entity (each has a different random wander target). // Together they produce unique spread values for entities even when // 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) .wrapping_mul(1619) .wrapping_add((cur_tile.y.unsigned_abs() as usize).wrapping_mul(31337)) diff --git a/src/world/generation/forestry.rs b/src/world/generation/forestry.rs index d69cfac..e9aa6c2 100644 --- a/src/world/generation/forestry.rs +++ b/src/world/generation/forestry.rs @@ -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 = 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::() % 100 == 0 { + if is_far_enough && rng.random::() % TREE_SPAWN_CHANCE == 0 { // Generate trunk - let trunk_height = 4 + rng.random::() % 5; + let trunk_height = TREE_TRUNK_MIN_HEIGHT + + rng.random::() % 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::() * 0.35 - 0.1)); + * (1.0 + + (rng.random::() + * 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) diff --git a/src/world/generation/forestry/magic_numbers.rs b/src/world/generation/forestry/magic_numbers.rs new file mode 100644 index 0000000..940d726 --- /dev/null +++ b/src/world/generation/forestry/magic_numbers.rs @@ -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; diff --git a/src/world/generation/terrain.rs b/src/world/generation/terrain.rs index b7a1f79..ef51b9c 100644 --- a/src/world/generation/terrain.rs +++ b/src/world/generation/terrain.rs @@ -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, diff --git a/src/world/generation/terrain/magic_numbers.rs b/src/world/generation/terrain/magic_numbers.rs new file mode 100644 index 0000000..26fd22e --- /dev/null +++ b/src/world/generation/terrain/magic_numbers.rs @@ -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; diff --git a/src/world/tiles/chunk_data.rs b/src/world/tiles/chunk_data.rs index 6038a93..05e48bf 100644 --- a/src/world/tiles/chunk_data.rs +++ b/src/world/tiles/chunk_data.rs @@ -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); } } diff --git a/src/world/tiles/magic_numbers.rs b/src/world/tiles/magic_numbers.rs new file mode 100644 index 0000000..a5667ad --- /dev/null +++ b/src/world/tiles/magic_numbers.rs @@ -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; diff --git a/src/world/tiles/mod.rs b/src/world/tiles/mod.rs index 3aae045..8c44aa2 100644 --- a/src/world/tiles/mod.rs +++ b/src/world/tiles/mod.rs @@ -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; diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs index 3616bd1..5cd24ef 100644 --- a/src/world/tiles/tilemap.rs +++ b/src/world/tiles/tilemap.rs @@ -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. diff --git a/src/world/tiles/tilemap_chunk.rs b/src/world/tiles/tilemap_chunk.rs index 1ffb0e0..d62c130 100644 --- a/src/world/tiles/tilemap_chunk.rs +++ b/src/world/tiles/tilemap_chunk.rs @@ -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(