Files
dorf/last_24h_diff.patch
T
popertots 0550191c7a Fix update_wandering_targets target validation logic
- Restored original target selection logic that validates is_standable_tile
- Fixed target position z-level checking (check floor tile then standable)
- Re-added check for completed paths (path_index >= path.len)
- This was the cause of 50,297 failed paths - entities targeting invalid positions
2026-03-18 15:45:25 +00:00

2381 lines
85 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Vec3>,
pub step_recovery: u32,
}
+
+#[derive(Component)]
+pub struct PendingPath {
+ pub start: IVec3,
+ pub goal: IVec3,
+ pub waypoint_path: Vec<Vec3>,
+ 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<Vec3>)>,
+}
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<Vec<u128>> = const{ RefCell::new(Vec::new()) } ;
+ static LOCAL_PATH_LENGTHS: RefCell<Vec<usize>> = const{ RefCell::new(Vec::new()) };
+ static LOCAL_NODES_EXPANDED: RefCell<Vec<usize>> = const{ RefCell::new(Vec::new()) };
+ static LOCAL_FAILED_PATHS: RefCell<u64> = 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<FxHashMap<IVec3, i32>> = RefCell::new(FxHashMap::default());
+ static SCRATCH_CAME_FROM_FX: RefCell<FxHashMap<IVec3, IVec3>> = RefCell::new(FxHashMap::default());
+ static SCRATCH_CLOSED_SET_FX: RefCell<HashSet<IVec3>> = RefCell::new(HashSet::default());
+ static SCRATCH_IN_OPEN_FX: RefCell<HashSet<IVec3>> = RefCell::new(HashSet::default());
+ static SCRATCH_G_SCORES_AH: RefCell<AHashMap<IVec3, i32>> = RefCell::new(AHashMap::default());
+ static SCRATCH_CAME_FROM_AH: RefCell<AHashMap<IVec3, IVec3>> = RefCell::new(AHashMap::default());
+ static SCRATCH_CLOSED_SET_AH: RefCell<AHashSet<IVec3>> = RefCell::new(AHashSet::default());
+ static SCRATCH_IN_OPEN_AH: RefCell<AHashSet<IVec3>> = RefCell::new(AHashSet::default());
+ static SCRATCH_TIER0_G_POS: RefCell<Vec<IVec3>> = RefCell::new(Vec::with_capacity(256));
+ static SCRATCH_TIER0_G_VAL: RefCell<Vec<i32>> = RefCell::new(Vec::with_capacity(256));
+ static SCRATCH_TIER0_CF_POS: RefCell<Vec<IVec3>> = RefCell::new(Vec::with_capacity(256));
+ static SCRATCH_TIER0_CF_PREV: RefCell<Vec<IVec3>> = RefCell::new(Vec::with_capacity(256));
+ static SCRATCH_TIER0_CLOSED: RefCell<Vec<IVec3>> = 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<u128>,
+ pub path_lengths: Vec<usize>,
+ pub nodes_expanded: Vec<usize>,
+
+ // System-level metrics
+ pub movement_system_times_us: Vec<u128>,
+ pub wander_system_times_us: Vec<u128>,
+
+ // 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<crate::entities::shared_components::CompletedPaths>,
+ 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::<crate::entities::shared_components::PendingPath>();
+ }
+ }
}
}
+/// 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<PathfindingBenchmark>) {
+ 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<TileMap>,
@@ -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<Vec3> {
- 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<Vec3> {
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<Vec3> {
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<Vec3> {
in_open_set.remove(&current);
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<Vec3> {
// 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<Vec3> {
_ => continue,
};
- let new_g = g_scores.get(&current).unwrap_or(&i32::MAX) + movement_cost;
+ let new_g = *g_scores.get(&current).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<Vec3> {
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<IVec3, IVec3>, mut current: IVec3) -> Vec<Vec3> {
+fn reconstruct_path(came_from: &FxHashMap<IVec3, IVec3>, mut current: IVec3) -> Vec<Vec3> {
let mut path = vec![Vec3::new(
current.x as f32,
current.y as f32,
@@ -381,3 +545,1080 @@ fn reconstruct_path(came_from: HashMap<IVec3, IVec3>, 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<ButtonInput<KeyCode>>,
+ mut bench: ResMut<PathfindingBenchmark>,
+) {
+ 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::<Vec<_>>(),
+ );
+ report_stat(
+ "nodes_expanded",
+ &bench
+ .nodes_expanded
+ .iter()
+ .map(|&n| n as u128)
+ .collect::<Vec<_>>(),
+ );
+
+ 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::<u128>() / 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<Vec3> {
+ let max_nodes = 256usize;
+ let mut g_positions: Vec<IVec3> = Vec::with_capacity(max_nodes);
+ let mut g_values: Vec<i32> = Vec::with_capacity(max_nodes);
+ let mut came_from_positions: Vec<IVec3> = Vec::with_capacity(max_nodes);
+ let mut came_from_prev: Vec<IVec3> = Vec::with_capacity(max_nodes);
+ let mut closed_positions: Vec<IVec3> = Vec::with_capacity(max_nodes);
+ let mut open_set: BinaryHeap<PathNode> = 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<Vec3> {
+ 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(&current);
+ 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(&current).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<Vec3> {
+ 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<IVec3, IVec3> = AHashMap::with_capacity(estimated_nodes);
+ let mut g_scores: AHashMap<IVec3, i32> = AHashMap::with_capacity(estimated_nodes);
+ let mut closed_set: AHashSet<IVec3> = AHashSet::with_capacity(estimated_nodes);
+ let mut in_open_set: AHashSet<IVec3> = 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(&current);
+ 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(&current).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<IVec3> {
+ 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<IVec3, IVec3>, mut current: IVec3) -> Vec<Vec3> {
+ 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(&current) {
+ 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<Vec3> {
+ 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<Vec3> = 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<Vec3> {
+ 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<Vec3> {
+ 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<Vec3>, 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<PathNode> = 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<Vec3>, 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(&current);
+ 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(&current).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<Vec3>, 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<IVec3, IVec3> = AHashMap::with_capacity(estimated_nodes);
+ let mut g_scores: AHashMap<IVec3, i32> = AHashMap::with_capacity(estimated_nodes);
+ let mut closed_set: AHashSet<IVec3> = AHashSet::with_capacity(estimated_nodes);
+ let mut in_open_set: AHashSet<IVec3> = 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(&current);
+ 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(&current).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<Vec3>, 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<Vec3> = 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<Vec3> {
+ 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<Vec3> {
+ 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<IVec3> = (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<Vec<Vec3>> = (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<Vec<(IVec3, (i32, bool, bool, [u32; 8]))>> =
- Mutex::new(Vec::<(IVec3, (i32, bool, bool, [u32; 8]))>::new());
+ let collected_tilemap_updates: Mutex<Vec<(IVec3, FixtureTileData)>> = 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<IVec3, (i32, bool, bool, bool, i32, [u32; 8])> =
- HashMap::new();
+ let mut local_tilemap_updates: HashMap<IVec3, FloorTileData> = 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<GenerateChunkEvent>) {
- 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<IVec3, (i32, bool, bool, bool, i32, [u32; 8])>, //id, canStandIn, canStandOn, visiblyTransparent, astar_weight, visible_range
- pub fixture_tiles: HashMap<IVec3, (i32, bool, bool, [u32; 8])>, // id, canStandIn, canStandOn, visible_range
- pub item_tiles: HashMap<IVec3, Vec<u32>>, // Entity.id's of items on this tile
+ pub floor_tiles: Arc<AHashMap<IVec3, FloorTileData>>,
+ pub fixture_tiles: Arc<AHashMap<IVec3, FixtureTileData>>,
+ pub item_tiles: Arc<AHashMap<IVec3, Vec<u32>>>,
+}
+
+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<Vec<u32>> {
+ 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;
}
}