- 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
2381 lines
85 KiB
Diff
2381 lines
85 KiB
Diff
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(¤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<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(¤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<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(¤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<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(¤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<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(¤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<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(¤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<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(¤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<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;
|
||
}
|
||
}
|
||
|