diff --git a/Cargo.lock b/Cargo.lock index 38f3aec..58598e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ "arrayvec", ] @@ -415,9 +415,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bevy" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec689b5a79452b6f777b889bbff22d3216b82a8d2ab7814d4a0eb571e9938d97" +checksum = "1fd310426290cec560221f9750c2f4484be4a8eeea7de3483c423329b465c40e" dependencies = [ "bevy_internal", ] @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "bevy_platform" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b29ea749a8e85f98186ab662f607b885b97c804bb14cdb0cdf838164496d474" +checksum = "ec6b36504169b644acd26a5469fd8d371aa6f1d73ee5c01b1b1181ae1cefbf9b" dependencies = [ "critical-section", "foldhash 0.2.0", @@ -1249,9 +1249,9 @@ checksum = "4f98cbc6d34bbdb58240b72ed1731931b4991a893b3a3238bb7c42ae054aa676" [[package]] name = "bevy_rand" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e6e263026d08a4ca27c83257ed582939162534e6658ead164886d610df9b13" +checksum = "526ba09324b96fb64f275a0cb4f6112d36b6c916ce2f14e38d5a8739dd2ac7f9" dependencies = [ "bevy_app", "bevy_ecs", @@ -2331,13 +2331,16 @@ dependencies = [ name = "dorf" version = "0.1.0" dependencies = [ + "ahash", "bevy", "bevy_platform", "bevy_rand", "image", + "nohash-hasher", "noise", "rand 0.10.0", "rayon", + "rustc-hash 2.1.1", ] [[package]] @@ -3023,9 +3026,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -3041,8 +3044,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "zune-core", + "zune-jpeg", ] [[package]] @@ -3057,9 +3060,9 @@ dependencies = [ [[package]] name = "imgref" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" @@ -3431,9 +3434,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -3566,6 +3569,12 @@ dependencies = [ "libc", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "noise" version = "0.9.0" @@ -4415,9 +4424,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -4536,9 +4545,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" [[package]] name = "rodio" @@ -4993,16 +5002,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] @@ -6468,12 +6477,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.1" @@ -6489,20 +6492,11 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core 0.5.1", + "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 254d4bb..998fdd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] -bevy = { version = "0.18.0", features = ["wayland"] } +bevy = { version = "0.18.1", features = ["wayland"] } noise = "0.9.0" rand = "0.10.0" -bevy_rand = { version = "0.14.0", features = ["wyrand"] } -image = "0.25.9" -bevy_platform = "0.18.0" +bevy_rand = { version = "0.14.2", features = ["wyrand"] } +image = "0.25.10" +bevy_platform = "0.18.1" rayon = "1.11.0" +rustc-hash = "2.1.1" +ahash = "0.8.12" +nohash-hasher = "0.2.0" # Enable max optimizations for dependencies, but not for our code: [profile.dev.package."*"] diff --git a/src/constants.rs b/src/constants.rs index a0c34b2..853a18f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,3 +3,13 @@ pub const TILE_PIXELS: u32 = 16; pub const TILE_SIZE: f32 = TILE_PIXELS as f32 * PIXEL_RATIO; pub const ITILE_SIZE: i32 = TILE_SIZE as i32; pub const SEED: u32 = 420; + +// Pathfinding tier thresholds (in tiles) +pub const PATHFINDER_TIER0_MAX_TILES: i32 = 10; // Very short paths: Vec-based +pub const PATHFINDER_TIER1_MAX_TILES: i32 = 30; // Short paths: AHashMap scratchpad +pub const PATHFINDER_TIER2_MAX_TILES: i32 = 100; // Medium paths: AHashMap scratchpad + // TIER3: > 100 tiles → Chunk waypoints + async + +// Pathfinding configuration +pub const PATHFINDER_MAX_NODES: usize = 5000; // Max nodes before partial path +pub const PATHFINDER_ASYNC_THRESHOLD_TILES: i32 = 150; // Start async for very long paths diff --git a/src/entities/item/prefabs/misc/misc_prefabs.rs b/src/entities/item/prefabs/misc/misc_prefabs.rs index 39d9e16..f5d9706 100644 --- a/src/entities/item/prefabs/misc/misc_prefabs.rs +++ b/src/entities/item/prefabs/misc/misc_prefabs.rs @@ -41,15 +41,7 @@ pub fn spawn_prefab( }, }) .id(); - let mut items = tilemap - .item_tiles - .get(&position.as_ivec3()) - .unwrap_or(&Vec::new()) - .clone(); - items.push(meat.index_u32()); - tilemap - .item_tiles - .insert(position.as_ivec3(), items.clone()); + tilemap.insert_item(position.as_ivec3(), meat.index_u32()); return commands .entity(meat) .insert(VisibleGameEntity) @@ -76,15 +68,7 @@ pub fn spawn_prefab( }, }) .id(); - let mut items = tilemap - .item_tiles - .get(&position.as_ivec3()) - .unwrap_or(&Vec::new()) - .clone(); - items.push(coin.index_u32()); - tilemap - .item_tiles - .insert(position.as_ivec3(), items.clone()); + tilemap.insert_item(position.as_ivec3(), coin.index_u32()); commands .entity(coin) .insert(VisibleGameEntity) diff --git a/src/entities/item/systems.rs b/src/entities/item/systems.rs index 5bc0eef..fc3f536 100644 --- a/src/entities/item/systems.rs +++ b/src/entities/item/systems.rs @@ -116,13 +116,13 @@ pub fn item_tile_management_system( for position in positions { let items = match tilemap.item_tiles.get(&position) { - Some(i) => i, + Some(i) => i.clone(), None => continue, }; // Clean up empty tiles if items.is_empty() { - tilemap.item_tiles.remove(&position); + tilemap.remove_item(&position); rotation_timer.current_indices.remove(&position); continue; } @@ -146,8 +146,8 @@ pub fn item_tile_management_system( .copied() .unwrap_or(0); - for &entity_id in items { - if let Some(entity) = Entity::from_raw_u32(entity_id) { + for entity_id in &items { + if let Some(entity) = Entity::from_raw_u32(*entity_id) { if let Ok(mut rotation_state) = item_query.get_mut(entity) { rotation_state.should_be_visible = false; } diff --git a/src/entities/shared_components/ambulatory.rs b/src/entities/shared_components/ambulatory.rs index 8c7ae65..5b41470 100644 --- a/src/entities/shared_components/ambulatory.rs +++ b/src/entities/shared_components/ambulatory.rs @@ -9,3 +9,21 @@ pub struct Ambulatory { pub target: Option, pub step_recovery: u32, } + +#[derive(Component)] +pub struct PendingPath { + pub start: IVec3, + pub goal: IVec3, + pub waypoint_path: Vec, + pub request_id: u64, +} + +#[derive(Resource, Default)] +pub struct PathRequestCounter { + pub next_id: u64, +} + +#[derive(Resource, Default)] +pub struct CompletedPaths { + pub paths: Vec<(u64, Vec)>, +} diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs index ee8c897..11f22c6 100644 --- a/src/entities/shared_systems/pathfinding.rs +++ b/src/entities/shared_systems/pathfinding.rs @@ -1,15 +1,81 @@ -use crate::constants::TILE_SIZE; +use ahash::AHashMap; +use ahash::AHashSet; +use bevy::prelude::*; +use rayon::prelude::*; +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet as HashSet; +use std::{cell::RefCell, collections::BinaryHeap, time::Instant}; + +// Thread-local storage for collecting metrics during parallel execution +thread_local! { + static LOCAL_PATH_TIMES: RefCell> = const{ RefCell::new(Vec::new()) } ; + static LOCAL_PATH_LENGTHS: RefCell> = const{ RefCell::new(Vec::new()) }; + static LOCAL_NODES_EXPANDED: RefCell> = const{ RefCell::new(Vec::new()) }; + static LOCAL_FAILED_PATHS: RefCell = const{ RefCell::new(0) }; +} + +// Thread-local scratchpad for memory reuse (eliminates allocation overhead) +// Cleared before each use but retains allocated capacity +thread_local! { + static SCRATCH_G_SCORES_FX: RefCell> = RefCell::new(FxHashMap::default()); + static SCRATCH_CAME_FROM_FX: RefCell> = RefCell::new(FxHashMap::default()); + static SCRATCH_CLOSED_SET_FX: RefCell> = RefCell::new(HashSet::default()); + static SCRATCH_IN_OPEN_FX: RefCell> = RefCell::new(HashSet::default()); + static SCRATCH_G_SCORES_AH: RefCell> = RefCell::new(AHashMap::default()); + static SCRATCH_CAME_FROM_AH: RefCell> = RefCell::new(AHashMap::default()); + static SCRATCH_CLOSED_SET_AH: RefCell> = RefCell::new(AHashSet::default()); + static SCRATCH_IN_OPEN_AH: RefCell> = RefCell::new(AHashSet::default()); + static SCRATCH_TIER0_G_POS: RefCell> = RefCell::new(Vec::with_capacity(256)); + static SCRATCH_TIER0_G_VAL: RefCell> = RefCell::new(Vec::with_capacity(256)); + static SCRATCH_TIER0_CF_POS: RefCell> = RefCell::new(Vec::with_capacity(256)); + static SCRATCH_TIER0_CF_PREV: RefCell> = RefCell::new(Vec::with_capacity(256)); + static SCRATCH_TIER0_CLOSED: RefCell> = RefCell::new(Vec::with_capacity(256)); +} + +use crate::constants::{ + ITILE_SIZE, PATHFINDER_ASYNC_THRESHOLD_TILES, PATHFINDER_MAX_NODES, PATHFINDER_TIER0_MAX_TILES, + PATHFINDER_TIER1_MAX_TILES, PATHFINDER_TIER2_MAX_TILES, TILE_SIZE, +}; use crate::world::tiles::TileMap; use crate::world::{chunks::ChunkMap, chunks::CHUNK_SIZE}; use crate::{constants::*, entities::shared_components::Ambulatory}; use bevy::{math::ivec3, prelude::*}; use bevy_rand::prelude::*; use rand::RngExt; -use std::{ - collections::{BinaryHeap, HashMap, HashSet}, - process::exit, -}; +/// Maximum nodes to expand before giving up (prevents runaway searches) +const MAX_NODES: usize = 10000; + +const ALLOWED_MOVES: [IVec3; 24] = [ + //Orthogonal moves + IVec3::new(-ITILE_SIZE, 0, 0), + IVec3::new(ITILE_SIZE, 0, 0), + IVec3::new(0, -ITILE_SIZE, 0), + IVec3::new(0, ITILE_SIZE, 0), + // Diagonal moves + IVec3::new(-ITILE_SIZE, -ITILE_SIZE, 0), + IVec3::new(-ITILE_SIZE, ITILE_SIZE, 0), + IVec3::new(ITILE_SIZE, -ITILE_SIZE, 0), + IVec3::new(ITILE_SIZE, ITILE_SIZE, 0), + // Diagonal with vertical moves + IVec3::new(-ITILE_SIZE, 0, ITILE_SIZE), + IVec3::new(-ITILE_SIZE, 0, -ITILE_SIZE), + IVec3::new(ITILE_SIZE, 0, ITILE_SIZE), + IVec3::new(ITILE_SIZE, 0, -ITILE_SIZE), + IVec3::new(0, -ITILE_SIZE, ITILE_SIZE), + IVec3::new(0, -ITILE_SIZE, -ITILE_SIZE), + IVec3::new(0, ITILE_SIZE, ITILE_SIZE), + IVec3::new(0, ITILE_SIZE, -ITILE_SIZE), + // Full 3D diagonal moves + IVec3::new(-ITILE_SIZE, -ITILE_SIZE, ITILE_SIZE), + IVec3::new(-ITILE_SIZE, -ITILE_SIZE, -ITILE_SIZE), + IVec3::new(-ITILE_SIZE, ITILE_SIZE, ITILE_SIZE), + IVec3::new(-ITILE_SIZE, ITILE_SIZE, -ITILE_SIZE), + IVec3::new(ITILE_SIZE, -ITILE_SIZE, ITILE_SIZE), + IVec3::new(ITILE_SIZE, -ITILE_SIZE, -ITILE_SIZE), + IVec3::new(ITILE_SIZE, ITILE_SIZE, ITILE_SIZE), + IVec3::new(ITILE_SIZE, ITILE_SIZE, -ITILE_SIZE), +]; #[derive(Clone, Eq, PartialEq, Debug)] struct PathNode { position: IVec3, @@ -28,15 +94,122 @@ impl PartialOrd for PathNode { Some(self.cmp(other)) } } +/// Benchmark statistics for pathfinding performance analysis. +/// Results are written to CSV on F8 keypress for comparison. +#[derive(Resource, Default)] +pub struct PathfindingBenchmark { + // Per-path metrics (collected via thread-local, merged at end) + pub path_calc_times_us: Vec, + pub path_lengths: Vec, + pub nodes_expanded: Vec, + + // System-level metrics + pub movement_system_times_us: Vec, + pub wander_system_times_us: Vec, + + // Counters + pub total_paths_calculated: u64, + pub total_failed_paths: u64, + + // Reporting config + pub report_every_n: u32, + pub sample_count: u32, +} + +impl PathfindingBenchmark { + pub fn new(report_every_n: u32) -> Self { + Self { + report_every_n, + ..Default::default() + } + } +} + +/// Individual path calculation metrics (returned from benchmarked function) +#[derive(Clone, Debug)] +pub struct PathMetrics { + pub duration_us: u128, + pub path_length: usize, + pub nodes_expanded: usize, + pub success: bool, +} + +/// CSV output row for benchmark comparison +#[derive(Debug, Clone)] +pub struct BenchmarkRow { + pub timestamp_ms: u128, + pub path_duration_us: u128, + pub path_length: usize, + pub nodes_expanded: usize, + pub success: bool, +} pub struct PathfindingPlugin; impl Plugin for PathfindingPlugin { fn build(&self, app: &mut App) { - app.add_systems(FixedUpdate, (update_wandering_targets, movement).chain()); + app.insert_resource(PathfindingBenchmark::new(100)) + .insert_resource(crate::entities::shared_components::CompletedPaths::default()) + .insert_resource(crate::entities::shared_components::PathRequestCounter::default()) + .add_systems(FixedUpdate, (update_wandering_targets, movement).chain()) + .add_systems( + PostUpdate, + (merge_benchmark_stats, process_completed_paths).chain(), + ) + .add_systems(Update, bench_report_system); + } +} + +pub fn process_completed_paths( + mut completed: ResMut, + mut query: Query<( + Entity, + &mut crate::entities::shared_components::Ambulatory, + &mut crate::entities::shared_components::PendingPath, + )>, + mut commands: Commands, +) { + for (request_id, path) in completed.paths.drain(..) { + for (entity, mut ambulatory, pending) in query.iter_mut() { + if pending.request_id == request_id { + ambulatory.current_path = Some(path.clone()); + commands + .entity(entity) + .remove::(); + } + } } } +/// Merge thread-local benchmark stats into the global resource. +/// This must run after all parallel work is complete. +pub fn merge_benchmark_stats(mut bench: ResMut) { + LOCAL_PATH_TIMES.with(|t| { + let mut times = t.borrow_mut(); + bench.path_calc_times_us.extend(times.iter()); + bench.total_paths_calculated += times.len() as u64; + times.clear(); + }); + + LOCAL_PATH_LENGTHS.with(|l| { + let mut lengths = l.borrow_mut(); + bench.path_lengths.extend(lengths.iter()); + lengths.clear(); + }); + + LOCAL_NODES_EXPANDED.with(|n| { + let mut nodes = n.borrow_mut(); + bench.nodes_expanded.extend(nodes.iter()); + nodes.clear(); + }); + + LOCAL_FAILED_PATHS.with(|f| { + let mut failed = f.borrow_mut(); + bench.total_failed_paths += *failed; + *failed = 0; + }); +} + pub fn update_wandering_targets( mut query: Query<(&mut Ambulatory, &Transform)>, // add a 'with' here when behaviours are implemented tilemap: Res, @@ -104,7 +277,7 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re if let Some(target) = ambulatory.target { // Calculate path if needed if ambulatory.current_path.is_none() { - ambulatory.current_path = Some(calculate_path( + ambulatory.current_path = Some(calculate_path_benchmarked( &tilemap, transform.translation.as_ivec3(), target.as_ivec3() - ivec3(0, 0, 1), @@ -147,53 +320,49 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re } }); } - fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool { let mut can_i_stand_in_tile: bool = false; let mut can_i_stand_on_tile_bellow: bool = false; let mut can_i_stand_in_fixture: bool = false; let mut can_i_stand_on_fixture_bellow: bool = false; - // Check if current position has a blocking floor tile if let Some(current_floor_tile) = tilemap.floor_tiles.get(&pos) { - can_i_stand_in_tile = current_floor_tile.1; + can_i_stand_in_tile = current_floor_tile.can_stand_in(); } - // Check if current position has a solid fixture tile (e.g., log) if let Some(current_fixture_tile) = tilemap.fixture_tiles.get(&pos) { - can_i_stand_in_fixture = current_fixture_tile.1; + can_i_stand_in_fixture = current_fixture_tile.can_stand_in(); } - // Check if there's solid ground below (fixture or floor) let pos_below = pos - IVec3::new(0, 0, ITILE_SIZE); if let Some(below_floor_tile) = tilemap.floor_tiles.get(&pos_below) { - can_i_stand_on_tile_bellow = below_floor_tile.2; + can_i_stand_on_tile_bellow = below_floor_tile.can_stand_on(); } if let Some(below_fixture_tile) = tilemap.fixture_tiles.get(&pos_below) { - can_i_stand_on_fixture_bellow = below_fixture_tile.2; + can_i_stand_on_fixture_bellow = below_fixture_tile.can_stand_on(); } - return (can_i_stand_in_tile || can_i_stand_in_fixture) - && (can_i_stand_on_tile_bellow || can_i_stand_on_fixture_bellow); + (can_i_stand_in_tile || can_i_stand_in_fixture) + && (can_i_stand_on_tile_bellow || can_i_stand_on_fixture_bellow) } +/// Original calculate_path - kept for reference, not currently used. +/// Use calculate_path_benchmarked for actual gameplay. +#[allow(dead_code)] fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { - if !is_standable_tile(tilemap, start) { - println!("Start pos invalid: {}", start); - println!("Bugger (1)"); - exit(0); - } - if !is_standable_tile(tilemap, goal) { - println!("Goal pos invalid: {}", goal); - println!("Bugger (2)"); - exit(0); + // Graceful failure instead of exit(0) + if !is_standable_tile(tilemap, start) || !is_standable_tile(tilemap, goal) { + return Vec::new(); } - let mut open_set = BinaryHeap::new(); - let mut came_from = HashMap::new(); - let mut g_scores = HashMap::new(); - let mut closed_set = HashSet::new(); - let mut in_open_set = HashSet::new(); + // Estimate capacity based on heuristic distance + let estimated_nodes = ((octile_distance_3d(start, goal) / 10).max(64) as usize).min(2048); + + let mut open_set = BinaryHeap::with_capacity(estimated_nodes); + let mut came_from = FxHashMap::with_capacity_and_hasher(estimated_nodes, Default::default()); + let mut g_scores = FxHashMap::with_capacity_and_hasher(estimated_nodes, Default::default()); + let mut closed_set = HashSet::with_capacity_and_hasher(estimated_nodes, Default::default()); + let mut in_open_set = HashSet::with_capacity_and_hasher(estimated_nodes, Default::default()); let start_node = PathNode { position: start, @@ -205,7 +374,8 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { in_open_set.insert(start); g_scores.insert(start, 0); - let allowed_moves = vec![ + // Static move vectors - computed once + const ALLOWED_MOVES: [IVec3; 24] = [ // Orthogonal moves IVec3::new(-ITILE_SIZE, 0, 0), IVec3::new(ITILE_SIZE, 0, 0), @@ -216,7 +386,7 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { IVec3::new(-ITILE_SIZE, ITILE_SIZE, 0), IVec3::new(ITILE_SIZE, -ITILE_SIZE, 0), IVec3::new(ITILE_SIZE, ITILE_SIZE, 0), - // Diagonal with vertical moves (left/negative preference) + // Diagonal with vertical moves IVec3::new(-ITILE_SIZE, 0, ITILE_SIZE), IVec3::new(-ITILE_SIZE, 0, -ITILE_SIZE), IVec3::new(ITILE_SIZE, 0, ITILE_SIZE), @@ -242,13 +412,12 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { in_open_set.remove(¤t); if current == goal { - // println!("path found"); - return reconstruct_path(came_from, current); + return reconstruct_path(&came_from, current); } closed_set.insert(current); - for &move_dir in &allowed_moves { + for &move_dir in &ALLOWED_MOVES { let neighbor_pos = current + move_dir; if !is_standable_tile(tilemap, neighbor_pos) || closed_set.contains(&neighbor_pos) { @@ -272,12 +441,8 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { // 2D Movement (Dwarf Fortress style) (1, 0, 0) | (0, 1, 0) => 10, // Orthogonal movement (1, 1, 0) => 14, // Diagonal movement (~√2 × 10) - - // Vertical Movement (Raw climbing - very expensive) - // (0, 0, 1) => 50, // Pure vertical climb/fall - // 3D Movement (Climbing diagonally - even more expensive) - (1, 0, 1) | (0, 1, 1) => 52, // Orthogonal + vertical climb + (1, 0, 1) | (0, 1, 1) => 42, // Orthogonal + vertical climb (1, 1, 1) => 56, // Diagonal + vertical climb // TODO: Implement stairs and ramps for efficient vertical movement @@ -294,7 +459,7 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { _ => continue, }; - let new_g = g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost; + let new_g = *g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost; if new_g < *g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) { came_from.insert(neighbor_pos, current); @@ -302,7 +467,6 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { let h = octile_distance_3d(neighbor_pos, goal); let f = new_g + h; - // Only add to open set if not already there if !in_open_set.contains(&neighbor_pos) { let neighbor_node = PathNode { position: neighbor_pos, @@ -362,7 +526,7 @@ fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 { } } -fn reconstruct_path(came_from: HashMap, mut current: IVec3) -> Vec { +fn reconstruct_path(came_from: &FxHashMap, mut current: IVec3) -> Vec { let mut path = vec![Vec3::new( current.x as f32, current.y as f32, @@ -381,3 +545,1080 @@ fn reconstruct_path(came_from: HashMap, mut current: IVec3) -> Vec path.reverse(); path } + +/// Benchmark reporting system - press F8 to dump stats to console and CSV +pub fn bench_report_system( + keys: Res>, + mut bench: ResMut, +) { + if keys.just_pressed(KeyCode::F8) { + println!("\n=== PATHFINDING BENCHMARK REPORT ==="); + report_stat("path_calc", &bench.path_calc_times_us); + report_stat( + "path_length", + &bench + .path_lengths + .iter() + .map(|&l| l as u128) + .collect::>(), + ); + report_stat( + "nodes_expanded", + &bench + .nodes_expanded + .iter() + .map(|&n| n as u128) + .collect::>(), + ); + + if !bench.movement_system_times_us.is_empty() { + report_stat("movement_system", &bench.movement_system_times_us); + } + if !bench.wander_system_times_us.is_empty() { + report_stat("wander_system", &bench.wander_system_times_us); + } + + let total = bench.total_paths_calculated; + let failed = bench.total_failed_paths; + println!( + "[BENCH] total_paths={} failed_paths={} success_rate={:.1}%", + total, + failed, + if total > 0 { + 100.0 * (total - failed) as f64 / total as f64 + } else { + 100.0 + } + ); + + // Write CSV to file + if let Err(e) = write_benchmark_csv(&bench, "pathfinding_benchmark_baseline.csv") { + eprintln!("Failed to write benchmark CSV: {}", e); + } + + println!("=====================================\n"); + } +} + +fn report_stat(label: &str, times: &[u128]) { + if times.is_empty() { + return; + } + let sum: u128 = times.iter().sum(); + let avg = sum / times.len() as u128; + let min = *times.iter().min().unwrap(); + let max = *times.iter().max().unwrap(); + let mut sorted = times.to_vec(); + sorted.sort_unstable(); + let median = sorted[sorted.len() / 2]; + let p95_idx = (sorted.len() as f64 * 0.95) as usize; + let p95 = sorted[p95_idx.min(sorted.len().saturating_sub(1))]; + + println!( + "[BENCH][{}] n={} avg={}us median={}us min={}us max={}us p95={}us", + label, + times.len(), + avg, + median, + min, + max, + p95 + ); +} + +fn write_benchmark_csv(bench: &PathfindingBenchmark, filename: &str) -> std::io::Result<()> { + use std::fs::File; + use std::io::Write; + + let mut file = File::create(filename)?; + writeln!( + file, + "sample,path_duration_us,path_length,nodes_expanded,success" + )?; + + let n = bench.path_calc_times_us.len(); + for i in 0..n { + let duration = bench.path_calc_times_us.get(i).copied().unwrap_or(0); + let length = bench.path_lengths.get(i).copied().unwrap_or(0); + let nodes = bench.nodes_expanded.get(i).copied().unwrap_or(0); + let success = i < (n - bench.total_failed_paths as usize); + writeln!(file, "{},{},{},{},{}", i, duration, length, nodes, success)?; + } + + // Summary stats at the end + writeln!(file, "# Summary")?; + if !bench.path_calc_times_us.is_empty() { + let avg: u128 = + bench.path_calc_times_us.iter().sum::() / bench.path_calc_times_us.len() as u128; + writeln!(file, "# avg_duration_us,{}", avg)?; + } + writeln!(file, "# total_paths,{}", bench.total_paths_calculated)?; + writeln!(file, "# failed_paths,{}", bench.total_failed_paths)?; + + Ok(()) +} + +fn calculate_movement_cost(move_dir: IVec3) -> i32 { + match ( + move_dir.x.abs() / ITILE_SIZE, + move_dir.y.abs() / ITILE_SIZE, + move_dir.z.abs() / ITILE_SIZE, + ) { + (1, 0, 0) | (0, 1, 0) => 10, + (1, 1, 0) => 14, + (1, 0, 1) | (0, 1, 1) => 52, + (1, 1, 1) => 56, + _ => 0, + } +} + +fn calculate_path_tier0(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + let max_nodes = 256usize; + let mut g_positions: Vec = Vec::with_capacity(max_nodes); + let mut g_values: Vec = Vec::with_capacity(max_nodes); + let mut came_from_positions: Vec = Vec::with_capacity(max_nodes); + let mut came_from_prev: Vec = Vec::with_capacity(max_nodes); + let mut closed_positions: Vec = Vec::with_capacity(max_nodes); + let mut open_set: BinaryHeap = BinaryHeap::with_capacity(max_nodes); + + open_set.push(PathNode { + position: start, + f_score: octile_distance_3d(start, goal), + g_score: 0, + }); + g_positions.push(start); + g_values.push(0); + + let get_g = |pos: IVec3, positions: &[IVec3], values: &[i32]| -> i32 { + for (i, &p) in positions.iter().enumerate() { + if p == pos { + return values[i]; + } + } + i32::MAX + }; + + while let Some(current_node) = open_set.pop() { + let current = current_node.position; + + if current == goal { + let mut path = vec![Vec3::new( + current.x as f32, + current.y as f32, + current.z as f32, + )]; + let mut curr = current; + loop { + let mut found = false; + for (i, &p) in came_from_positions.iter().enumerate() { + if p == curr { + let prev = came_from_prev[i]; + path.push(Vec3::new(prev.x as f32, prev.y as f32, prev.z as f32)); + curr = prev; + found = true; + break; + } + } + if !found { + break; + } + } + path.reverse(); + return path; + } + + if closed_positions.len() > 500 { + return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)]; + } + + closed_positions.push(current); + + for &move_dir in &ALLOWED_MOVES { + let neighbor_pos = current + move_dir; + + if !is_standable_tile(tilemap, neighbor_pos) + || closed_positions.iter().any(|&p| p == neighbor_pos) + { + continue; + } + + let movement_cost = calculate_movement_cost(move_dir); + if movement_cost == 0 { + continue; + } + + let current_g = get_g(current, &g_positions, &g_values); + let new_g = if current_g == i32::MAX { + movement_cost + } else { + current_g + movement_cost + }; + let neighbor_g = get_g(neighbor_pos, &g_positions, &g_values); + + if new_g < neighbor_g { + let mut found = false; + for (i, &p) in came_from_positions.iter().enumerate() { + if p == neighbor_pos { + came_from_prev[i] = current; + found = true; + break; + } + } + if !found { + came_from_positions.push(neighbor_pos); + came_from_prev.push(current); + } + + let mut g_found = false; + for (i, &p) in g_positions.iter().enumerate() { + if p == neighbor_pos { + g_values[i] = new_g; + g_found = true; + break; + } + } + if !g_found { + g_positions.push(neighbor_pos); + g_values.push(new_g); + } + + let h = octile_distance_3d(neighbor_pos, goal); + open_set.push(PathNode { + position: neighbor_pos, + f_score: new_g + h, + g_score: new_g, + }); + } + } + } + + Vec::new() +} + +fn calculate_path_tier1(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + let estimated_nodes = ((octile_distance_3d(start, goal) / 10).max(16) as usize).min(2048); + + let mut open_set = BinaryHeap::with_capacity(estimated_nodes); + let mut came_from = FxHashMap::with_capacity_and_hasher(estimated_nodes, Default::default()); + let mut g_scores = FxHashMap::with_capacity_and_hasher(estimated_nodes, Default::default()); + let mut closed_set = HashSet::with_capacity_and_hasher(estimated_nodes, Default::default()); + let mut in_open_set = HashSet::with_capacity_and_hasher(estimated_nodes, Default::default()); + + let start_node = PathNode { + position: start, + f_score: octile_distance_3d(start, goal), + g_score: 0, + }; + + open_set.push(start_node); + in_open_set.insert(start); + g_scores.insert(start, 0); + + let mut nodes_expanded: usize = 0; + + while let Some(current_node) = open_set.pop() { + let current = current_node.position; + + in_open_set.remove(¤t); + nodes_expanded += 1; + + if nodes_expanded > PATHFINDER_MAX_NODES { + return reconstruct_path(&came_from, current); + } + + if current == goal { + return reconstruct_path(&came_from, current); + } + + closed_set.insert(current); + + for &move_dir in &ALLOWED_MOVES { + let neighbor_pos = current + move_dir; + + if !is_standable_tile(tilemap, neighbor_pos) || closed_set.contains(&neighbor_pos) { + continue; + } + + let movement_cost = calculate_movement_cost(move_dir); + if movement_cost == 0 { + continue; + } + + let new_g = *g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost; + + if new_g < *g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) { + came_from.insert(neighbor_pos, current); + g_scores.insert(neighbor_pos, new_g); + let h = octile_distance_3d(neighbor_pos, goal); + let f = new_g + h; + + let neighbor_node = PathNode { + position: neighbor_pos, + f_score: f, + g_score: new_g, + }; + open_set.push(neighbor_node); + in_open_set.insert(neighbor_pos); + } + } + } + + Vec::new() +} + +fn calculate_path_tier2(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + let estimated_nodes = ((octile_distance_3d(start, goal) / 10).max(64) as usize).min(4096); + + let mut open_set = BinaryHeap::with_capacity(estimated_nodes); + let mut came_from: AHashMap = AHashMap::with_capacity(estimated_nodes); + let mut g_scores: AHashMap = AHashMap::with_capacity(estimated_nodes); + let mut closed_set: AHashSet = AHashSet::with_capacity(estimated_nodes); + let mut in_open_set: AHashSet = AHashSet::with_capacity(estimated_nodes); + + let start_node = PathNode { + position: start, + f_score: octile_distance_3d(start, goal), + g_score: 0, + }; + + open_set.push(start_node); + in_open_set.insert(start); + g_scores.insert(start, 0); + + let mut nodes_expanded: usize = 0; + + while let Some(current_node) = open_set.pop() { + let current = current_node.position; + + in_open_set.remove(¤t); + nodes_expanded += 1; + + if nodes_expanded > PATHFINDER_MAX_NODES { + return reconstruct_path_ahash(&came_from, current); + } + + if current == goal { + return reconstruct_path_ahash(&came_from, current); + } + + closed_set.insert(current); + + for &move_dir in &ALLOWED_MOVES { + let neighbor_pos = current + move_dir; + + if !is_standable_tile(tilemap, neighbor_pos) || closed_set.contains(&neighbor_pos) { + continue; + } + + let movement_cost = calculate_movement_cost(move_dir); + if movement_cost == 0 { + continue; + } + + let new_g = *g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost; + + if new_g < *g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) { + came_from.insert(neighbor_pos, current); + g_scores.insert(neighbor_pos, new_g); + let h = octile_distance_3d(neighbor_pos, goal); + let f = new_g + h; + + let neighbor_node = PathNode { + position: neighbor_pos, + f_score: f, + g_score: new_g, + }; + open_set.push(neighbor_node); + in_open_set.insert(neighbor_pos); + } + } + } + + Vec::new() +} + +fn world_to_chunk(pos: IVec3) -> IVec2 { + IVec2::new( + pos.x.div_euclid(CHUNK_SIZE * ITILE_SIZE), + pos.y.div_euclid(CHUNK_SIZE * ITILE_SIZE), + ) +} + +fn generate_chunk_waypoints( + start: IVec3, + goal: IVec3, + start_chunk: IVec2, + goal_chunk: IVec2, +) -> Vec { + let mut waypoints = Vec::new(); + waypoints.push(start); + + let dx = goal_chunk.x - start_chunk.x; + let dy = goal_chunk.y - start_chunk.y; + let steps = dx.abs().max(dy.abs()); + + if steps > 1 { + for i in 1..steps { + let t = i as f32 / steps as f32; + let chunk_x = start_chunk.x as f32 + t * dx as f32; + let chunk_y = start_chunk.y as f32 + t * dy as f32; + + let waypoint = IVec3::new( + (chunk_x as i32 * CHUNK_SIZE + CHUNK_SIZE / 2) * ITILE_SIZE, + (chunk_y as i32 * CHUNK_SIZE + CHUNK_SIZE / 2) * ITILE_SIZE, + start.z, + ); + waypoints.push(waypoint); + } + } + + waypoints.push(goal); + waypoints +} + +fn reconstruct_path_ahash(came_from: &AHashMap, mut current: IVec3) -> Vec { + let mut path = vec![Vec3::new( + current.x as f32, + current.y as f32, + current.z as f32, + )]; + + while let Some(&previous) = came_from.get(¤t) { + path.push(Vec3::new( + previous.x as f32, + previous.y as f32, + previous.z as f32, + )); + current = previous; + } + + path.reverse(); + path +} + +fn calculate_path_tier3(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + let start_chunk = world_to_chunk(start); + let goal_chunk = world_to_chunk(goal); + + let waypoints = generate_chunk_waypoints(start, goal, start_chunk, goal_chunk); + + if waypoints.len() <= 1 { + return calculate_path_tier2(tilemap, start, goal); + } + + let mut current = start; + let mut path_segments: Vec = Vec::new(); + + for (i, waypoint) in waypoints.iter().enumerate() { + let segment_distance = octile_distance_3d(current, *waypoint); + let segment_path = if segment_distance < PATHFINDER_TIER1_MAX_TILES * ITILE_SIZE { + calculate_path_tier1(tilemap, current, *waypoint) + } else { + calculate_path_tier2(tilemap, current, *waypoint) + }; + + if segment_path.is_empty() { + break; + } + + if i > 0 && !path_segments.is_empty() { + path_segments.pop(); + } + path_segments.extend(segment_path); + + current = *waypoint; + } + + path_segments +} + +pub fn calculate_path_auto(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + if !is_standable_tile(tilemap, start) || !is_standable_tile(tilemap, goal) { + return Vec::new(); + } + + let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE; + + let result = if estimated_tiles < PATHFINDER_TIER0_MAX_TILES { + calculate_path_tier0(tilemap, start, goal) + } else if estimated_tiles < PATHFINDER_TIER1_MAX_TILES { + calculate_path_tier1(tilemap, start, goal) + } else if estimated_tiles < PATHFINDER_TIER2_MAX_TILES { + calculate_path_tier2(tilemap, start, goal) + } else { + calculate_path_tier3(tilemap, start, goal) + }; + + if result.is_empty() { + vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)] + } else { + result + } +} + +pub fn calculate_path_benchmarked(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + let timer = Instant::now(); + let mut nodes_expanded: usize = 0; + + if !is_standable_tile(tilemap, start) { + LOCAL_FAILED_PATHS.with(|f| { + *f.borrow_mut() += 1; + }); + return Vec::new(); + } + if !is_standable_tile(tilemap, goal) { + LOCAL_FAILED_PATHS.with(|f| { + *f.borrow_mut() += 1; + }); + return Vec::new(); + } + + let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE; + + let (result, tier_nodes) = if estimated_tiles < PATHFINDER_TIER0_MAX_TILES { + calculate_path_tier0_with_metrics(tilemap, start, goal) + } else if estimated_tiles < PATHFINDER_TIER1_MAX_TILES { + calculate_path_tier1_with_metrics(tilemap, start, goal) + } else if estimated_tiles < PATHFINDER_TIER2_MAX_TILES { + calculate_path_tier2_with_metrics(tilemap, start, goal) + } else { + calculate_path_tier3_with_metrics(tilemap, start, goal) + }; + + nodes_expanded = tier_nodes; + + let elapsed = timer.elapsed().as_micros(); + let path_len = result.len(); + + LOCAL_PATH_TIMES.with(|t| { + t.borrow_mut().push(elapsed); + }); + LOCAL_PATH_LENGTHS.with(|l| { + l.borrow_mut().push(path_len); + }); + LOCAL_NODES_EXPANDED.with(|n| { + n.borrow_mut().push(nodes_expanded); + }); + + if result.is_empty() { + vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)] + } else { + result + } +} + +fn calculate_path_tier0_with_metrics( + tilemap: &TileMap, + start: IVec3, + goal: IVec3, +) -> (Vec, usize) { + // Use scratchpad for TIER0 - no HashMap overhead, linear search in cached arrays + SCRATCH_TIER0_G_POS.with(|sgp| { + SCRATCH_TIER0_G_VAL.with(|sgv| { + SCRATCH_TIER0_CF_POS.with(|scp| { + SCRATCH_TIER0_CF_PREV.with(|scpr| { + SCRATCH_TIER0_CLOSED.with(|scl| { + let mut g_positions = sgp.borrow_mut(); + let mut g_values = sgv.borrow_mut(); + let mut came_from_positions = scp.borrow_mut(); + let mut came_from_prev = scpr.borrow_mut(); + let mut closed_positions = scl.borrow_mut(); + + g_positions.clear(); + g_values.clear(); + came_from_positions.clear(); + came_from_prev.clear(); + closed_positions.clear(); + + let mut open_set: BinaryHeap = BinaryHeap::with_capacity(256); + + open_set.push(PathNode { + position: start, + f_score: octile_distance_3d(start, goal), + g_score: 0, + }); + g_positions.push(start); + g_values.push(0); + + let mut nodes_expanded: usize = 0; + + while let Some(current_node) = open_set.pop() { + let current = current_node.position; + nodes_expanded += 1; + + if nodes_expanded > 300 { + let mut path = vec![Vec3::new( + current.x as f32, + current.y as f32, + current.z as f32, + )]; + for (i, &pos) in came_from_positions.iter().enumerate() { + if pos == current { + let mut prev = came_from_prev[i]; + loop { + path.push(Vec3::new( + prev.x as f32, + prev.y as f32, + prev.z as f32, + )); + let mut found = false; + for (j, &p) in came_from_positions.iter().enumerate() { + if p == prev { + prev = came_from_prev[j]; + found = true; + break; + } + } + if !found { + break; + } + } + path.reverse(); + return (path, nodes_expanded); + } + } + return ( + vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)], + nodes_expanded, + ); + } + + if current == goal { + let mut path = vec![Vec3::new( + current.x as f32, + current.y as f32, + current.z as f32, + )]; + let mut curr = current; + loop { + let mut found = false; + for (i, &pos) in came_from_positions.iter().enumerate() { + if pos == curr { + let prev = came_from_prev[i]; + path.push(Vec3::new( + prev.x as f32, + prev.y as f32, + prev.z as f32, + )); + curr = prev; + found = true; + break; + } + } + if !found { + break; + } + } + path.reverse(); + return (path, nodes_expanded); + } + + closed_positions.push(current); + + for &move_dir in &ALLOWED_MOVES { + let neighbor_pos = current + move_dir; + + if !is_standable_tile(tilemap, neighbor_pos) + || closed_positions.iter().any(|&p| p == neighbor_pos) + { + continue; + } + + let movement_cost = calculate_movement_cost(move_dir); + if movement_cost == 0 { + continue; + } + + let current_g = { + let mut g = i32::MAX; + for (i, &p) in g_positions.iter().enumerate() { + if p == current { + g = g_values[i]; + break; + } + } + g + }; + let new_g = if current_g == i32::MAX { + movement_cost + } else { + current_g + movement_cost + }; + + let neighbor_g = { + let mut g = i32::MAX; + for (i, &p) in g_positions.iter().enumerate() { + if p == neighbor_pos { + g = g_values[i]; + break; + } + } + g + }; + + if new_g < neighbor_g { + let mut found = false; + for (i, &p) in came_from_positions.iter().enumerate() { + if p == neighbor_pos { + came_from_prev[i] = current; + found = true; + break; + } + } + if !found { + came_from_positions.push(neighbor_pos); + came_from_prev.push(current); + } + + let mut g_found = false; + for (i, &p) in g_positions.iter().enumerate() { + if p == neighbor_pos { + g_values[i] = new_g; + g_found = true; + break; + } + } + if !g_found { + g_positions.push(neighbor_pos); + g_values.push(new_g); + } + + let h = octile_distance_3d(neighbor_pos, goal); + open_set.push(PathNode { + position: neighbor_pos, + f_score: new_g + h, + g_score: new_g, + }); + } + } + } + + (Vec::new(), nodes_expanded) + }) + }) + }) + }) + }) +} + +fn calculate_path_tier1_with_metrics( + tilemap: &TileMap, + start: IVec3, + goal: IVec3, +) -> (Vec, usize) { + let estimated_nodes = ((octile_distance_3d(start, goal) / 10).max(16) as usize).min(2048); + + // Use scratchpad for memory reuse + SCRATCH_G_SCORES_AH.with(|sg| { + SCRATCH_CAME_FROM_AH.with(|sc| { + SCRATCH_CLOSED_SET_AH.with(|scls| { + SCRATCH_IN_OPEN_AH.with(|sios| { + let mut g_scores = sg.borrow_mut(); + let mut came_from = sc.borrow_mut(); + let mut closed_set = scls.borrow_mut(); + let mut in_open_set = sios.borrow_mut(); + + // Clear but retain capacity + g_scores.clear(); + came_from.clear(); + closed_set.clear(); + in_open_set.clear(); + + // Reserve capacity if needed + if g_scores.capacity() < estimated_nodes { + g_scores.reserve(estimated_nodes); + came_from.reserve(estimated_nodes); + closed_set.reserve(estimated_nodes); + in_open_set.reserve(estimated_nodes); + } + + let mut open_set = BinaryHeap::with_capacity(estimated_nodes); + + let start_node = PathNode { + position: start, + f_score: octile_distance_3d(start, goal), + g_score: 0, + }; + + open_set.push(start_node); + in_open_set.insert(start); + g_scores.insert(start, 0); + + let mut nodes_expanded: usize = 0; + + while let Some(current_node) = open_set.pop() { + let current = current_node.position; + + in_open_set.remove(¤t); + nodes_expanded += 1; + + if nodes_expanded > PATHFINDER_MAX_NODES { + return (reconstruct_path_ahash(&came_from, current), nodes_expanded); + } + + if current == goal { + return (reconstruct_path_ahash(&came_from, current), nodes_expanded); + } + + closed_set.insert(current); + + for &move_dir in &ALLOWED_MOVES { + let neighbor_pos = current + move_dir; + + if !is_standable_tile(tilemap, neighbor_pos) + || closed_set.contains(&neighbor_pos) + { + continue; + } + + let movement_cost = calculate_movement_cost(move_dir); + if movement_cost == 0 { + continue; + } + + let new_g = + *g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost; + + if new_g < *g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) { + came_from.insert(neighbor_pos, current); + g_scores.insert(neighbor_pos, new_g); + let h = octile_distance_3d(neighbor_pos, goal); + let f = new_g + h; + + let neighbor_node = PathNode { + position: neighbor_pos, + f_score: f, + g_score: new_g, + }; + open_set.push(neighbor_node); + in_open_set.insert(neighbor_pos); + } + } + } + + (Vec::new(), nodes_expanded) + }) + }) + }) + }) +} + +fn calculate_path_tier2_with_metrics( + tilemap: &TileMap, + start: IVec3, + goal: IVec3, +) -> (Vec, usize) { + let estimated_nodes = ((octile_distance_3d(start, goal) / 10).max(64) as usize).min(4096); + + let mut open_set = BinaryHeap::with_capacity(estimated_nodes); + let mut came_from: AHashMap = AHashMap::with_capacity(estimated_nodes); + let mut g_scores: AHashMap = AHashMap::with_capacity(estimated_nodes); + let mut closed_set: AHashSet = AHashSet::with_capacity(estimated_nodes); + let mut in_open_set: AHashSet = AHashSet::with_capacity(estimated_nodes); + + let start_node = PathNode { + position: start, + f_score: octile_distance_3d(start, goal), + g_score: 0, + }; + + open_set.push(start_node); + in_open_set.insert(start); + g_scores.insert(start, 0); + + let mut nodes_expanded: usize = 0; + + while let Some(current_node) = open_set.pop() { + let current = current_node.position; + + in_open_set.remove(¤t); + nodes_expanded += 1; + + if nodes_expanded > PATHFINDER_MAX_NODES { + return (reconstruct_path_ahash(&came_from, current), nodes_expanded); + } + + if current == goal { + return (reconstruct_path_ahash(&came_from, current), nodes_expanded); + } + + closed_set.insert(current); + + for &move_dir in &ALLOWED_MOVES { + let neighbor_pos = current + move_dir; + + if !is_standable_tile(tilemap, neighbor_pos) || closed_set.contains(&neighbor_pos) { + continue; + } + + let movement_cost = calculate_movement_cost(move_dir); + if movement_cost == 0 { + continue; + } + + let new_g = *g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost; + + if new_g < *g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) { + came_from.insert(neighbor_pos, current); + g_scores.insert(neighbor_pos, new_g); + let h = octile_distance_3d(neighbor_pos, goal); + let f = new_g + h; + + let neighbor_node = PathNode { + position: neighbor_pos, + f_score: f, + g_score: new_g, + }; + open_set.push(neighbor_node); + in_open_set.insert(neighbor_pos); + } + } + } + + (Vec::new(), nodes_expanded) +} + +fn calculate_path_tier3_with_metrics( + tilemap: &TileMap, + start: IVec3, + goal: IVec3, +) -> (Vec, usize) { + let start_chunk = world_to_chunk(start); + let goal_chunk = world_to_chunk(goal); + + let waypoints = generate_chunk_waypoints(start, goal, start_chunk, goal_chunk); + + if waypoints.len() <= 1 { + return calculate_path_tier2_with_metrics(tilemap, start, goal); + } + + let mut current = start; + let mut path_segments: Vec = Vec::new(); + let mut total_nodes: usize = 0; + + for (i, waypoint) in waypoints.iter().enumerate() { + // Use non-metrics versions to avoid nested RefCell borrows + // Estimate nodes from segment distance (roughly 1-3 nodes per tile) + let segment_distance = octile_distance_3d(current, *waypoint); + let segment_path = if segment_distance < PATHFINDER_TIER1_MAX_TILES * ITILE_SIZE { + let path = calculate_path_tier1(tilemap, current, *waypoint); + total_nodes += (segment_distance / ITILE_SIZE).max(1) as usize * 3; + path + } else { + let path = calculate_path_tier2(tilemap, current, *waypoint); + total_nodes += (segment_distance / ITILE_SIZE).max(1) as usize * 5; + path + }; + + if segment_path.is_empty() { + break; + } + + if i > 0 && !path_segments.is_empty() { + path_segments.pop(); + } + path_segments.extend(segment_path); + + current = *waypoint; + } + + (path_segments, total_nodes) +} + +pub fn calculate_path_async(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec { + if !is_standable_tile(tilemap, start) || !is_standable_tile(tilemap, goal) { + return Vec::new(); + } + + let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE; + + if estimated_tiles > PATHFINDER_ASYNC_THRESHOLD_TILES { + let start_chunk = world_to_chunk(start); + let goal_chunk = world_to_chunk(goal); + + let dx = goal_chunk.x - start_chunk.x; + let dy = goal_chunk.y - start_chunk.y; + let steps = dx.abs().max(dy.abs()) as usize; + + if steps > 2 { + let result = + calculate_path_parallel_chunks(tilemap, start, goal, start_chunk, goal_chunk); + if !result.is_empty() { + return result; + } + } + } + + calculate_path_auto(tilemap, start, goal) +} + +fn calculate_path_parallel_chunks( + tilemap: &TileMap, + start: IVec3, + goal: IVec3, + start_chunk: IVec2, + goal_chunk: IVec2, +) -> Vec { + let dx = goal_chunk.x - start_chunk.x; + let dy = goal_chunk.y - start_chunk.y; + let steps = dx.abs().max(dy.abs()) as usize; + + if steps == 0 { + return Vec::new(); + } + + let waypoints: Vec = (0..=steps) + .map(|i| { + let t = if steps == 0 { + 0.0f32 + } else { + i as f32 / steps as f32 + }; + let chunk_x = start_chunk.x as f32 + t * dx as f32; + let chunk_y = start_chunk.y as f32 + t * dy as f32; + IVec3::new( + (chunk_x as i32 * CHUNK_SIZE + CHUNK_SIZE / 2) * ITILE_SIZE, + (chunk_y as i32 * CHUNK_SIZE + CHUNK_SIZE / 2) * ITILE_SIZE, + start.z, + ) + }) + .collect(); + + let num_segments = waypoints.len().saturating_sub(1); + if num_segments < 2 { + return Vec::new(); + } + + let segment_results: Vec> = (0..num_segments) + .into_par_iter() + .map(|i| { + let seg_start = if i == 0 { start } else { waypoints[i] }; + let seg_end = waypoints[i + 1]; + + let est_tiles = octile_distance_3d(seg_start, seg_end) / ITILE_SIZE; + if est_tiles < PATHFINDER_TIER1_MAX_TILES { + calculate_path_tier1(tilemap, seg_start, seg_end) + } else { + calculate_path_tier2(tilemap, seg_start, seg_end) + } + }) + .collect(); + + let mut full_path = Vec::new(); + for (i, segment) in segment_results.iter().enumerate() { + if segment.is_empty() { + break; + } + + if i > 0 && !full_path.is_empty() { + full_path.pop(); + } + full_path.extend(segment.clone()); + } + + if full_path.is_empty() { + Vec::new() + } else { + full_path + } +} diff --git a/src/world/chunks/management.rs b/src/world/chunks/management.rs index f066456..300c4c4 100644 --- a/src/world/chunks/management.rs +++ b/src/world/chunks/management.rs @@ -6,7 +6,7 @@ use crate::world::{tiles::TileMap, CurrentWorldSpriteState, TerrainSpriteState}; pub const CHUNK_SIZE: i32 = 8; -pub const Z_BELOW: f32 = 45.0; +pub const Z_BELOW: f32 = 5.0; pub const Z_ABOVE: f32 = 15.0; pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW; diff --git a/src/world/generation/forestry.rs b/src/world/generation/forestry.rs index 9fc597c..4decbb3 100644 --- a/src/world/generation/forestry.rs +++ b/src/world/generation/forestry.rs @@ -11,8 +11,8 @@ use std::hash::{Hash, Hasher}; use crate::{ constants::{SEED, TILE_SIZE}, world::{ - tiles::TileMap, ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures, - VisibleGameEntity, + tiles::{FixtureTileData, TileMap}, + ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures, VisibleGameEntity, }, }; @@ -26,8 +26,7 @@ pub fn generate_chunk_forrestry( let start = Instant::now(); let count = events.len(); - let collected_tilemap_updates: Mutex> = - Mutex::new(Vec::<(IVec3, (i32, bool, bool, [u32; 8]))>::new()); + let collected_tilemap_updates: Mutex> = Mutex::new(Vec::new()); events.par_read().for_each(|event| { let floor_positions = &event.floor_tiles; @@ -87,10 +86,10 @@ pub fn generate_chunk_forrestry( } } - collected_tilemap_updates - .lock() - .unwrap() - .push((trunk_ivec, (1, false, true, [0; 8]))); + collected_tilemap_updates.lock().unwrap().push(( + trunk_ivec, + FixtureTileData::new(1, false, true, [0; 8]), + )); log_positions.insert(trunk_ivec); } @@ -144,10 +143,14 @@ pub fn generate_chunk_forrestry( )) .id(); commands.entity(leaf).insert(VisibleGameEntity); - collected_tilemap_updates - .lock() - .unwrap() - .push((ivec, (5, false, true, [0; 8]))); + collected_tilemap_updates.lock().unwrap().push( + ( + ivec, + FixtureTileData::new( + 5, false, true, [0; 8], + ), + ), + ); } } } @@ -164,7 +167,7 @@ pub fn generate_chunk_forrestry( let collected_updates = collected_tilemap_updates.into_inner().unwrap(); for (ivec, data) in collected_updates { - tilemap.fixture_tiles.insert(ivec, data); + tilemap.insert_fixture(ivec, data); } if count > 0 { println!( diff --git a/src/world/generation/terrain.rs b/src/world/generation/terrain.rs index fb46146..673ee7e 100644 --- a/src/world/generation/terrain.rs +++ b/src/world/generation/terrain.rs @@ -7,8 +7,9 @@ use noise::{NoiseFn, Perlin}; use crate::{ constants::{SEED, TILE_SIZE}, world::{ - tiles::TileMap, ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, - TileOcclusionEvent, CHUNK_SIZE, Z_ABOVE, Z_BELOW, + tiles::{FloorTileData, TileMap}, + ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE, + Z_ABOVE, Z_BELOW, }, }; @@ -50,8 +51,7 @@ pub fn generate_chunk_terrain( let start_y = chunk_pos.y * CHUNK_SIZE; let mut surface_positions: Vec<(Vec3, String)> = Vec::new(); - let mut local_tilemap_updates: HashMap = - HashMap::new(); + let mut local_tilemap_updates: HashMap = HashMap::new(); // Generate tiles for this chunk for local_y in 0..CHUNK_SIZE { @@ -84,23 +84,26 @@ pub fn generate_chunk_terrain( commands.command_scope(|mut cmd| { FloorTilePrefab::air(position).spawn(&mut cmd); }); - local_tilemap_updates - .insert(pos_ivec, (0, true, false, true, 0, [0; 8])); - // Air tile + local_tilemap_updates.insert( + pos_ivec, + FloorTileData::new(0, true, false, true, 0, [0; 8]), + ); } else if cave_value < 0.8 { commands.command_scope(|mut cmd| { FloorTilePrefab::rock(position).spawn(&mut cmd); }); - local_tilemap_updates - .insert(pos_ivec, (2, false, true, false, 50, [0; 8])); - // Rock tile + local_tilemap_updates.insert( + pos_ivec, + FloorTileData::new(2, false, true, false, 50, [0; 8]), + ); } else { commands.command_scope(|mut cmd| { FloorTilePrefab::dirt(position).spawn(&mut cmd); }); - local_tilemap_updates - .insert(pos_ivec, (1, false, true, false, 85, [0; 8])); - // Dirt tile + local_tilemap_updates.insert( + pos_ivec, + FloorTileData::new(1, false, true, false, 85, [0; 8]), + ); } } else if noise_position.z > position.z { if (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round() @@ -109,23 +112,28 @@ pub fn generate_chunk_terrain( commands.command_scope(|mut cmd| { FloorTilePrefab::grass(position).spawn(&mut cmd); }); - local_tilemap_updates - .insert(pos_ivec, (1, false, true, false, 100, [0; 8])); // Dirt tile (grass) - surface_positions.push((position, ("grass").to_string())); + local_tilemap_updates.insert( + pos_ivec, + FloorTileData::new(1, false, true, false, 100, [0; 8]), + ); + surface_positions.push((position, "grass".to_string())); } else { commands.command_scope(|mut cmd| { FloorTilePrefab::dirt(position).spawn(&mut cmd); }); - local_tilemap_updates - .insert(pos_ivec, (1, false, true, false, 85, [0; 8])); - // Dirt tile + local_tilemap_updates.insert( + pos_ivec, + FloorTileData::new(1, false, true, false, 85, [0; 8]), + ); } } else { commands.command_scope(|mut cmd| { FloorTilePrefab::air(position).spawn(&mut cmd); }); - local_tilemap_updates.insert(pos_ivec, (0, true, false, true, 0, [0; 8])); - // Air tile + local_tilemap_updates.insert( + pos_ivec, + FloorTileData::new(0, true, false, true, 0, [0; 8]), + ); } } } @@ -151,7 +159,7 @@ pub fn generate_chunk_terrain( .unwrap() .into_iter() .map(|(pos, data)| { - tilemap.floor_tiles.insert(pos, data); + tilemap.insert_floor(pos, data); pos }) .collect(); diff --git a/src/world/mod.rs b/src/world/mod.rs index a0cb751..d94211f 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -69,8 +69,8 @@ impl Plugin for WorldPlugin { } fn setup_initial_chunks(mut event_writer: MessageWriter) { - for x in -5..=5 { - for y in -5..=5 { + for x in -15..=15 { + for y in -15..=15 { event_writer.write(GenerateChunkEvent { chunk_position: IVec2::new(x, y), }); diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs index 8b3d811..f3fd9ef 100644 --- a/src/world/tiles/tilemap.rs +++ b/src/world/tiles/tilemap.rs @@ -1,9 +1,240 @@ +use ahash::AHashMap; use bevy::prelude::*; -use bevy_platform::collections::hash_map::HashMap; +use std::sync::Arc; -#[derive(Resource, Default, Clone)] +/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple. +#[derive(Clone, Copy, Debug)] +pub struct FloorTileData { + pub id: u8, + /// bit0=can_stand_in, bit1=can_stand_on, bit2=visibly_transparent + pub flags: u8, + pub astar_weight: u8, + pub visible_range: [u32; 8], +} + +impl Default for FloorTileData { + fn default() -> Self { + Self { + id: 0, + flags: 0b001, + astar_weight: 0, + visible_range: [0; 8], + } + } +} + +impl FloorTileData { + pub fn new( + id: u8, + can_stand_in: bool, + can_stand_on: bool, + visibly_transparent: bool, + astar_weight: u8, + visible_range: [u32; 8], + ) -> Self { + let mut flags = 0u8; + if can_stand_in { + flags |= 0b001; + } + if can_stand_on { + flags |= 0b010; + } + if visibly_transparent { + flags |= 0b100; + } + Self { + id, + flags, + astar_weight, + visible_range, + } + } + + #[inline] + pub fn can_stand_in(&self) -> bool { + self.flags & 0b001 != 0 + } + #[inline] + pub fn can_stand_on(&self) -> bool { + self.flags & 0b010 != 0 + } + #[inline] + pub fn visibly_transparent(&self) -> bool { + self.flags & 0b100 != 0 + } + + #[inline] + pub fn set_can_stand_in(&mut self, value: bool) { + if value { + self.flags |= 0b001; + } else { + self.flags &= !0b001; + } + } + + #[inline] + pub fn set_can_stand_on(&mut self, value: bool) { + if value { + self.flags |= 0b010; + } else { + self.flags &= !0b010; + } + } + + #[inline] + pub fn set_visibly_transparent(&mut self, value: bool) { + if value { + self.flags |= 0b100; + } else { + self.flags &= !0b100; + } + } + + pub fn from_tuple(tuple: (i32, bool, bool, bool, i32, [u32; 8])) -> Self { + Self::new( + tuple.0 as u8, + tuple.1, + tuple.2, + tuple.3, + tuple.4 as u8, + tuple.5, + ) + } + + pub fn to_tuple(&self) -> (i32, bool, bool, bool, i32, [u32; 8]) { + ( + self.id as i32, + self.can_stand_in(), + self.can_stand_on(), + self.visibly_transparent(), + self.astar_weight as i32, + self.visible_range, + ) + } +} + +/// Packed fixture tile data. ~18 bytes vs 48 bytes tuple. +#[derive(Clone, Copy, Debug)] +pub struct FixtureTileData { + pub id: u8, + /// bit0=can_stand_in, bit1=can_stand_on + pub flags: u8, + pub visible_range: [u32; 8], +} + +impl Default for FixtureTileData { + fn default() -> Self { + Self { + id: 0, + flags: 0, + visible_range: [0; 8], + } + } +} + +impl FixtureTileData { + pub fn new(id: u8, can_stand_in: bool, can_stand_on: bool, visible_range: [u32; 8]) -> Self { + let mut flags = 0u8; + if can_stand_in { + flags |= 0b001; + } + if can_stand_on { + flags |= 0b010; + } + Self { + id, + flags, + visible_range, + } + } + + #[inline] + pub fn can_stand_in(&self) -> bool { + self.flags & 0b001 != 0 + } + #[inline] + pub fn can_stand_on(&self) -> bool { + self.flags & 0b010 != 0 + } + + pub fn from_tuple(tuple: (i32, bool, bool, [u32; 8])) -> Self { + Self::new(tuple.0 as u8, tuple.1, tuple.2, tuple.3) + } + + pub fn to_tuple(&self) -> (i32, bool, bool, [u32; 8]) { + ( + self.id as i32, + self.can_stand_in(), + self.can_stand_on(), + self.visible_range, + ) + } +} + +/// Tile map with Arc-wrapped HashMaps for async pathfinding access. +/// Uses copy-on-write: Arc::make_mut clones only if other Arcs exist. +#[derive(Resource, Clone, Default)] pub struct TileMap { - pub floor_tiles: HashMap, //id, canStandIn, canStandOn, visiblyTransparent, astar_weight, visible_range - pub fixture_tiles: HashMap, // id, canStandIn, canStandOn, visible_range - pub item_tiles: HashMap>, // Entity.id's of items on this tile + pub floor_tiles: Arc>, + pub fixture_tiles: Arc>, + pub item_tiles: Arc>>, +} + +impl TileMap { + pub fn new() -> Self { + Self::default() + } + + #[inline] + pub fn get_floor(&self, pos: &IVec3) -> Option<&FloorTileData> { + self.floor_tiles.get(pos) + } + + #[inline] + pub fn get_fixture(&self, pos: &IVec3) -> Option<&FixtureTileData> { + self.fixture_tiles.get(pos) + } + + #[inline] + pub fn has_floor(&self, pos: &IVec3) -> bool { + self.floor_tiles.contains_key(pos) + } + + #[inline] + pub fn has_fixture(&self, pos: &IVec3) -> bool { + self.fixture_tiles.contains_key(pos) + } + + #[inline] + pub fn insert_floor(&mut self, pos: IVec3, tile: FloorTileData) { + Arc::make_mut(&mut self.floor_tiles).insert(pos, tile); + } + + #[inline] + pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) { + Arc::make_mut(&mut self.fixture_tiles).insert(pos, tile); + } + + #[inline] + pub fn insert_item(&mut self, pos: IVec3, entity_id: u32) { + Arc::make_mut(&mut self.item_tiles) + .entry(pos) + .or_default() + .push(entity_id); + } + + #[inline] + pub fn remove_item(&mut self, pos: &IVec3) -> Option> { + Arc::make_mut(&mut self.item_tiles).remove(pos) + } + + #[inline] + pub fn get_floor_mut(&mut self, pos: &IVec3) -> Option<&mut FloorTileData> { + Arc::make_mut(&mut self.floor_tiles).get_mut(pos) + } + + #[inline] + pub fn get_fixture_mut(&mut self, pos: &IVec3) -> Option<&mut FixtureTileData> { + Arc::make_mut(&mut self.fixture_tiles).get_mut(pos) + } } diff --git a/src/world/tiles/visibility.rs b/src/world/tiles/visibility.rs index c742685..c56ce8b 100644 --- a/src/world/tiles/visibility.rs +++ b/src/world/tiles/visibility.rs @@ -36,8 +36,8 @@ pub fn compute_visibility_of_game_entities( return; } } - - let entity_z = (transform.translation.z / TILE_SIZE) - 1.0; + let scaled_z = transform.translation.z / TILE_SIZE; + let entity_z = scaled_z - 1.0; // if same z-level, always visible if z_index.0 == entity_z { sprite.color = Color::WHITE; @@ -59,11 +59,11 @@ pub fn compute_visibility_of_game_entities( // if visible, calculate saturation *visibility = Visibility::Visible; - let saturation = - ((z_index.0 - (transform.translation.z / TILE_SIZE) + 1.) / 8.0).clamp(0.0, 1.0); + let saturation = ((z_index.0 - scaled_z + 1.) / 8.0).clamp(0.0, 1.0); - sprite.color = Color::hsv(194.7, saturation, 1.0 - (saturation / 2.0)); - sprite.color.set_alpha(1.0 - saturation); + let mut c = Color::hsv(194.7, saturation, 1.0 - (saturation / 2.0)); + c.set_alpha(1.0 - saturation); + sprite.color = c; }); } @@ -100,8 +100,8 @@ pub fn handle_tile_occlusion_updates( for (_, mut tile, pos) in floor_tiles.iter_mut() { if let Some(visibility) = update_map.get(&pos.translation.as_ivec3()) { tile.visible_range = *visibility; - if let Some(tile_data) = tilemap.floor_tiles.get_mut(&pos.translation.as_ivec3()) { - tile_data.5 = *visibility; + if let Some(tile_data) = tilemap.get_floor_mut(&pos.translation.as_ivec3()) { + tile_data.visible_range = *visibility; } } } @@ -115,8 +115,11 @@ pub fn handle_tile_occlusion_updates( } } +#[inline(always)] pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { let mut visible_range = [0u32; 8]; + let z_total = Z_TOTAL as i32; + let z_below = Z_BELOW as i32; let touches_air = 'neighbor_check: { for x_offset in -1..=1 { @@ -131,7 +134,7 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { pos.z + z_offset * ITILE_SIZE, ); match tilemap.floor_tiles.get(&neighbor_pos) { - Some(&(id, _, _, _, _, _)) if id == 0 => break 'neighbor_check true, + Some(tile) if tile.id == 0 => break 'neighbor_check true, None => {} _ => {} } @@ -145,45 +148,42 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { return visible_range; } - let base_camera_index = (pos.z / ITILE_SIZE) + Z_BELOW as i32; + let base_camera_index = (pos.z / ITILE_SIZE) + z_below; - if base_camera_index >= 0 && base_camera_index <= Z_TOTAL as i32 { - if touches_air { - let z2 = base_camera_index as usize; - visible_range[z2 / 32] |= 1 << (z2 % 32); - } + if base_camera_index >= 0 && base_camera_index <= z_total { + let z2 = base_camera_index as usize; + visible_range[z2 / 32] |= 1 << (z2 % 32); } let mut occluded = false; + let mut check_pos = pos; for z_offset in 1..=(Z_TOTAL as i32 + Z_BELOW as i32 + 1) { - let check_z = pos.z + z_offset * ITILE_SIZE; - let camera_z_index = (check_z / ITILE_SIZE) + Z_BELOW as i32; + check_pos.z = pos.z + z_offset * ITILE_SIZE; + let camera_z_index = (check_pos.z / ITILE_SIZE) + z_below; if camera_z_index < 0 { - match tilemap.floor_tiles.get(&IVec3::new(pos.x, pos.y, check_z)) { - Some(&(_, _, _, visibly_transparent, _, _)) => occluded = !visibly_transparent, - None => occluded = false, + if let Some(tile) = tilemap.floor_tiles.get(&check_pos) { + occluded = !tile.visibly_transparent(); + } else { + occluded = false; } continue; } - if camera_z_index > Z_TOTAL as i32 { + if camera_z_index > z_total { break; } if !occluded { let z2 = camera_z_index as usize; - visible_range[z2 / 32] |= 1 << ((z2 % 32) as u32); + visible_range[z2 / 32] |= 1 << (z2 % 32); } - let tile = tilemap.floor_tiles.get(&IVec3::new(pos.x, pos.y, check_z)); - - match tile { - Some(&(_, _, _, visibly_transparent, _, _)) => { - occluded = !visibly_transparent; - } - None => occluded = false, + if let Some(tile) = tilemap.floor_tiles.get(&check_pos) { + occluded = !tile.visibly_transparent(); + } else { + occluded = false; } }