diff --git a/Cargo.lock b/Cargo.lock
index d58b8d3..440fc3a 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.1"
+version = "0.14.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c76e5db2c274081fdff32a525609436bd7ac2c24b91c1a5d3bc04e43d7558ddf"
+checksum = "526ba09324b96fb64f275a0cb4f6112d36b6c916ce2f14e38d5a8739dd2ac7f9"
 dependencies = [
  "bevy_app",
  "bevy_ecs",
@@ -2331,13 +2331,17 @@ dependencies = [
 name = "dorf"
 version = "0.1.0"
 dependencies = [
+ "ahash",
  "bevy",
  "bevy_platform",
  "bevy_rand",
+ "futures-lite",
  "image",
+ "nohash-hasher",
  "noise",
  "rand 0.10.0",
  "rayon",
+ "rustc-hash 2.1.1",
 ]
 
 [[package]]
@@ -3023,9 +3027,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 +3045,8 @@ dependencies = [
  "rayon",
  "rgb",
  "tiff",
- "zune-core 0.5.1",
- "zune-jpeg 0.5.12",
+ "zune-core",
+ "zune-jpeg",
 ]
 
 [[package]]
@@ -3057,9 +3061,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 +3435,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 +3570,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 +4425,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 +4546,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 +5003,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 +6478,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 +6493,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 ae71471..8522f33 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,13 +4,17 @@ 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.1", 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"
+futures-lite = "2.6"
 
 # 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..7a22b57 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -3,3 +3,8 @@ 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;
+
+pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
+pub const PATHFINDER_MAX_NODES: usize = 5000;
+pub const PATHFINDER_WAYPOINT_THRESHOLD_TILES: i32 = 100;
+pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 64;
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..36437a0 100644
--- a/src/entities/shared_systems/pathfinding.rs
+++ b/src/entities/shared_systems/pathfinding.rs
@@ -1,14 +1,91 @@
-use crate::constants::TILE_SIZE;
+use bevy::prelude::*;
+use rayon::prelude::*;
+use rustc_hash::FxHashMap;
+use rustc_hash::FxHashSet;
+use std::{cell::RefCell, collections::BinaryHeap, collections::VecDeque, time::Instant};
+
+use crate::constants::{
+    ITILE_SIZE, PATHFINDER_MAX_NODES, PATHFINDER_PROVISIONAL_NODE_LIMIT, 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::math::ivec3;
 use bevy_rand::prelude::*;
 use rand::RngExt;
-use std::{
-    collections::{BinaryHeap, HashMap, HashSet},
-    process::exit,
-};
+
+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) };
+}
+
+/// Single consolidated scratchpad for A* pathfinding.
+/// One RefCell borrow instead of multiple nested borrows.
+struct AStarScratchpad {
+    g_scores: FxHashMap<IVec3, i32>,
+    came_from: FxHashMap<IVec3, IVec3>,
+    closed_set: FxHashSet<IVec3>,
+    open_set: BinaryHeap<PathNode>,
+}
+
+impl Default for AStarScratchpad {
+    fn default() -> Self {
+        Self {
+            g_scores: FxHashMap::default(),
+            came_from: FxHashMap::default(),
+            closed_set: FxHashSet::default(),
+            open_set: BinaryHeap::new(),
+        }
+    }
+}
+
+impl AStarScratchpad {
+    fn clear_and_reserve(&mut self, capacity: usize) {
+        self.g_scores.clear();
+        self.came_from.clear();
+        self.closed_set.clear();
+        self.open_set.clear();
+
+        if self.g_scores.capacity() < capacity {
+            self.g_scores.reserve(capacity);
+            self.came_from.reserve(capacity);
+            self.closed_set.reserve(capacity);
+        }
+    }
+}
+
+thread_local! {
+    static SCRATCHPAD: RefCell<AStarScratchpad> = RefCell::new(AStarScratchpad::default());
+}
+
+const ALLOWED_MOVES: [IVec3; 24] = [
+    IVec3::new(-ITILE_SIZE, 0, 0),
+    IVec3::new(ITILE_SIZE, 0, 0),
+    IVec3::new(0, -ITILE_SIZE, 0),
+    IVec3::new(0, ITILE_SIZE, 0),
+    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),
+    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),
+    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 {
@@ -19,7 +96,10 @@ struct PathNode {
 
 impl Ord for PathNode {
     fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-        other.f_score.cmp(&self.f_score)
+        other
+            .f_score
+            .cmp(&self.f_score)
+            .then_with(|| other.g_score.cmp(&self.g_score))
     }
 }
 
@@ -29,58 +109,233 @@ impl PartialOrd for PathNode {
     }
 }
 
+#[derive(Resource, Default)]
+pub struct PathfindingBenchmark {
+    pub path_calc_times_us: Vec<u128>,
+    pub path_lengths: Vec<usize>,
+    pub nodes_expanded: Vec<usize>,
+    pub movement_system_times_us: Vec<u128>,
+    pub wander_system_times_us: Vec<u128>,
+    pub total_paths_calculated: u64,
+    pub total_failed_paths: u64,
+    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()
+        }
+    }
+}
+
+#[derive(Resource, Default)]
+pub struct PathRequestQueue {
+    pub pending: VecDeque<(Entity, IVec3, IVec3)>,
+}
+
+const MAX_PATHS_PER_FRAME: usize = 8;
+
 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())
+            .insert_resource(PathRequestQueue::default())
+            .add_systems(
+                FixedUpdate,
+                (prepare_paths, update_wandering_targets, movement).chain(),
+            )
+            .add_systems(
+                PostUpdate,
+                (
+                    merge_benchmark_stats,
+                    process_completed_paths,
+                    process_path_queue,
+                ),
+            )
+            .add_systems(Update, bench_report_system);
+    }
+}
+
+pub fn prepare_paths(
+    mut commands: Commands,
+    mut queue: ResMut<PathRequestQueue>,
+    mut query: Query<
+        (
+            Entity,
+            &mut crate::entities::shared_components::Ambulatory,
+            &Transform,
+        ),
+        Without<crate::entities::shared_components::PendingPath>,
+    >,
+    tilemap: Res<TileMap>,
+) {
+    for (entity, mut ambulatory, transform) in query.iter_mut() {
+        if ambulatory.current_path.is_some() || ambulatory.target.is_none() {
+            continue;
+        }
+        let Some(target) = ambulatory.target else {
+            continue;
+        };
+        let start = transform.translation.as_ivec3();
+        let goal = target.as_ivec3() - ivec3(0, 0, ITILE_SIZE);
+        let distance = octile_distance_3d(start, goal) / ITILE_SIZE;
+
+        if distance <= PATHFINDER_SHORT_PATH_MAX_TILES {
+            let path = calculate_path_benchmarked(&tilemap, start, goal);
+            ambulatory.current_path = Some(path);
+            ambulatory.path_index = 0;
+        } else {
+            let provisional = calculate_provisional_path(
+                &tilemap,
+                start,
+                goal,
+                PATHFINDER_PROVISIONAL_NODE_LIMIT,
+            );
+            if !provisional.is_empty() {
+                ambulatory.current_path = Some(provisional);
+                ambulatory.path_index = 0;
+
+                queue.pending.push_back((entity, start, goal));
+                commands
+                    .entity(entity)
+                    .insert(crate::entities::shared_components::PendingPath {
+                        start,
+                        goal,
+                        waypoint_path: Vec::new(),
+                        request_id: 0,
+                    });
+            } else {
+                let path = calculate_path_benchmarked(&tilemap, start, goal);
+                ambulatory.current_path = Some(path);
+                ambulatory.path_index = 0;
+            }
+        }
+    }
+}
+
+pub fn process_path_queue(
+    mut commands: Commands,
+    mut queue: ResMut<PathRequestQueue>,
+    tilemap: Res<TileMap>,
+    mut query: Query<
+        (Entity, &mut Ambulatory, &Transform),
+        With<crate::entities::shared_components::PendingPath>,
+    >,
+) {
+    let mut processed = 0;
+    while processed < MAX_PATHS_PER_FRAME {
+        if let Some((entity, _old_start, goal)) = queue.pending.pop_front() {
+            processed += 1;
+
+            if let Ok((_, mut ambulatory, transform)) = query.get_mut(entity) {
+                let actual_start = transform.translation.as_ivec3();
+                let full_path = calculate_path_benchmarked(&tilemap, actual_start, goal);
+
+                if !full_path.is_empty() {
+                    ambulatory.current_path = Some(full_path);
+                    ambulatory.path_index = 0;
+                }
+                commands
+                    .entity(entity)
+                    .remove::<crate::entities::shared_components::PendingPath>();
+            }
+        } else {
+            break;
+        }
+    }
+}
+
+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>();
+            }
+        }
     }
 }
 
+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
+    mut query: Query<(&mut Ambulatory, &Transform)>,
     tilemap: Res<TileMap>,
     chunk_map: Res<ChunkMap>,
     mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
 ) {
-    if let Ok(mut rng) = rng_q.single_mut() {
-        for (mut ambulatory, _) in query.iter_mut() {
-            if ambulatory.target.is_none()
-                || (ambulatory.current_path.is_some()
-                    && ambulatory.path_index >= ambulatory.current_path.as_ref().unwrap().len())
-            {
-                // Find a random loaded chunk
-                let loaded_chunks: Vec<&IVec2> = chunk_map.loaded_chunks.keys().collect();
-                if !loaded_chunks.is_empty() {
-                    let random_index = rng.random_range(0..loaded_chunks.len());
-                    if let Some(&chunk_pos) = loaded_chunks.get(random_index) {
-                        // Generate random position within chunk
-                        let chunk_x = chunk_pos.x * CHUNK_SIZE;
-                        let chunk_y = chunk_pos.y * CHUNK_SIZE;
-
-                        let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
-                        let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
-
-                        // Get height at position
-                        let surface_height = 0;
-
-                        // Find a valid z-level near the surface
-                        for z in (surface_height - 3)..=(surface_height + 4) {
-                            let mut target_pos = IVec3::new(target_x, target_y, z) * ITILE_SIZE;
-                            if let Some(_) = tilemap.floor_tiles.get(&target_pos) {
-                                target_pos.z += ITILE_SIZE;
-                                if let Some(_base_texture) = tilemap.floor_tiles.get(&target_pos) {
-                                    if is_standable_tile(&tilemap, target_pos) {
-                                        ambulatory.target = Some(Vec3::new(
-                                            target_pos.x as f32,
-                                            target_pos.y as f32,
-                                            target_pos.z as f32 + 1.0,
-                                        ));
-                                        ambulatory.current_path = None;
-                                        ambulatory.path_index = 0;
-                                        break;
-                                    }
-                                }
+    let Ok(mut rng) = rng_q.single_mut() else {
+        return;
+    };
+
+    for (mut ambulatory, _) in query.iter_mut() {
+        if ambulatory.target.is_none()
+            || (ambulatory.current_path.is_some()
+                && ambulatory.path_index >= ambulatory.current_path.as_ref().unwrap().len())
+        {
+            let loaded_chunks: Vec<&IVec2> = chunk_map.loaded_chunks.keys().collect();
+            if !loaded_chunks.is_empty() {
+                let random_index = rng.random_range(0..loaded_chunks.len());
+                if let Some(&chunk_pos) = loaded_chunks.get(random_index) {
+                    let chunk_x = chunk_pos.x * CHUNK_SIZE;
+                    let chunk_y = chunk_pos.y * CHUNK_SIZE;
+
+                    let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
+                    let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
+
+                    for z in -3..=4 {
+                        let mut target_pos = IVec3::new(target_x, target_y, z) * ITILE_SIZE;
+                        if tilemap.floor_tiles.get(&target_pos).is_some() {
+                            target_pos.z += ITILE_SIZE;
+                            if tilemap.floor_tiles.get(&target_pos).is_some()
+                                && is_standable_tile(&tilemap, target_pos)
+                            {
+                                ambulatory.target = Some(Vec3::new(
+                                    target_pos.x as f32,
+                                    target_pos.y as f32,
+                                    target_pos.z as f32 + 1.0,
+                                ));
+                                ambulatory.current_path = None;
+                                ambulatory.path_index = 0;
+                                break;
                             }
                         }
                     }
@@ -95,289 +350,435 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
         .par_iter_mut()
         .for_each(|(mut ambulatory, mut transform)| {
             let current_pos = transform.translation;
-            // Apply gravity if in air
             if !is_standable_tile(&tilemap, current_pos.as_ivec3()) {
                 transform.translation.z -= TILE_SIZE;
                 return;
             }
 
-            if let Some(target) = ambulatory.target {
-                // Calculate path if needed
-                if ambulatory.current_path.is_none() {
-                    ambulatory.current_path = Some(calculate_path(
-                        &tilemap,
-                        transform.translation.as_ivec3(),
-                        target.as_ivec3() - ivec3(0, 0, 1),
-                    ));
-                    ambulatory.path_index = 0;
-                }
-                if ambulatory.walk_speed > 0. {
-                    if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
-                        ambulatory.step_recovery += 1;
-                        return;
-                    } else {
-                        ambulatory.step_recovery = 0;
-                    }
-                }
+            if ambulatory.current_path.is_none() {
+                return;
+            }
 
-                // Follow the current path
-                if let Some(path) = &ambulatory.current_path {
-                    if ambulatory.path_index < path.len() {
-                        let next_point = path[ambulatory.path_index];
+            if ambulatory.walk_speed > 0. {
+                if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
+                    ambulatory.step_recovery += 1;
+                    return;
+                } else {
+                    ambulatory.step_recovery = 0;
+                }
+            }
 
-                        let direction = (next_point - transform.translation).normalize();
-                        transform.translation = next_point;
+            if let Some(path) = &ambulatory.current_path {
+                if ambulatory.path_index < path.len() {
+                    let next_point = path[ambulatory.path_index];
+                    let direction = (next_point - transform.translation).normalize();
+                    transform.translation = next_point;
 
-                        // Update sprite direction (only for x movement)
-                        if direction.x > 0.0 {
-                            transform.scale.x = PIXEL_RATIO;
-                        } else if direction.x < 0.0 {
-                            transform.scale.x = -PIXEL_RATIO;
-                        }
+                    if direction.x > 0.0 {
+                        transform.scale.x = PIXEL_RATIO;
+                    } else if direction.x < 0.0 {
+                        transform.scale.x = -PIXEL_RATIO;
+                    }
 
-                        // Check if we've reached the next point
-                        if transform.translation.distance(next_point) < TILE_SIZE {
-                            ambulatory.path_index += 1;
-                        }
-                    } else {
-                        ambulatory.current_path = None;
-                        ambulatory.target = None;
+                    if transform.translation.distance(next_point) < TILE_SIZE {
+                        ambulatory.path_index += 1;
                     }
+                } else {
+                    ambulatory.current_path = None;
+                    ambulatory.target = None;
                 }
             }
         });
 }
 
 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;
-    }
-    // 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;
-    }
-
-    // Check if there's solid ground below (fixture or floor)
+    let can_stand_in_tile = tilemap
+        .floor_tiles
+        .get(&pos)
+        .map(|t| t.can_stand_in())
+        .unwrap_or(false);
+    let can_stand_in_fixture = tilemap
+        .fixture_tiles
+        .get(&pos)
+        .map(|t| t.can_stand_in())
+        .unwrap_or(false);
     let pos_below = pos - IVec3::new(0, 0, ITILE_SIZE);
+    let can_stand_on_tile_below = tilemap
+        .floor_tiles
+        .get(&pos_below)
+        .map(|t| t.can_stand_on())
+        .unwrap_or(false);
+    let can_stand_on_fixture_below = tilemap
+        .fixture_tiles
+        .get(&pos_below)
+        .map(|t| t.can_stand_on())
+        .unwrap_or(false);
+
+    (can_stand_in_tile || can_stand_in_fixture)
+        && (can_stand_on_tile_below || can_stand_on_fixture_below)
+}
 
-    if let Some(below_floor_tile) = tilemap.floor_tiles.get(&pos_below) {
-        can_i_stand_on_tile_bellow = below_floor_tile.2;
+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) => 42,
+        (1, 1, 1) => 56,
+        _ => 0,
     }
+}
 
-    if let Some(below_fixture_tile) = tilemap.fixture_tiles.get(&pos_below) {
-        can_i_stand_on_fixture_bellow = below_fixture_tile.2;
-    }
-    return (can_i_stand_in_tile || can_i_stand_in_fixture)
-        && (can_i_stand_on_tile_bellow || can_i_stand_on_fixture_bellow);
+fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
+    let dx = (a.x - b.x).abs();
+    let dy = (a.y - b.y).abs();
+    let dz = (a.z - b.z).abs();
+    let (dmax, dmid, dmin) = sorted_desc(dx, dy, dz);
+    10 * dmax + 4 * dmid + dmin
 }
 
-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);
+fn sorted_desc(a: i32, b: i32, c: i32) -> (i32, i32, i32) {
+    let mut arr = [a, b, c];
+    arr.sort_unstable_by(|x, y| y.cmp(x));
+    (arr[0], arr[1], arr[2])
+}
+
+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,
+        current.z as f32,
+    )];
+    while let Some(&prev) = came_from.get(&current) {
+        path.push(Vec3::new(prev.x as f32, prev.y as f32, prev.z as f32));
+        current = prev;
     }
-    if !is_standable_tile(tilemap, goal) {
-        println!("Goal pos invalid: {}", goal);
-        println!("Bugger (2)");
-        exit(0);
+    path.reverse();
+    path
+}
+
+pub fn calculate_path_benchmarked(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
+    let timer = Instant::now();
+
+    if !is_standable_tile(tilemap, start) || !is_standable_tile(tilemap, goal) {
+        LOCAL_FAILED_PATHS.with(|f| {
+            *f.borrow_mut() += 1;
+        });
+        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();
+    let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
+    let (result, nodes_expanded) =
+        calculate_path_with_scratchpad(tilemap, start, goal, estimated_tiles);
+
+    let elapsed = timer.elapsed().as_micros();
+    LOCAL_PATH_TIMES.with(|t| {
+        t.borrow_mut().push(elapsed);
+    });
+    LOCAL_PATH_LENGTHS.with(|l| {
+        l.borrow_mut().push(result.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
+    }
+}
 
-    let start_node = PathNode {
-        position: start,
-        f_score: octile_distance_3d(start, goal),
-        g_score: 0,
-    };
+fn calculate_path_with_scratchpad(
+    tilemap: &TileMap,
+    start: IVec3,
+    goal: IVec3,
+    estimated_tiles: i32,
+) -> (Vec<Vec3>, usize) {
+    SCRATCHPAD.with(|s| {
+        let mut scratch = s.borrow_mut();
+        let capacity = ((estimated_tiles as usize).max(64)).min(4096);
+        scratch.clear_and_reserve(capacity);
+
+        let h = octile_distance_3d(start, goal);
+        scratch.open_set.push(PathNode {
+            position: start,
+            f_score: h,
+            g_score: 0,
+        });
+        scratch.g_scores.insert(start, 0);
 
-    open_set.push(start_node);
-    in_open_set.insert(start);
-    g_scores.insert(start, 0);
-
-    let allowed_moves = vec![
-        // 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 (left/negative preference)
-        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),
-    ];
-
-    while let Some(current_node) = open_set.pop() {
-        let current = current_node.position;
-
-        in_open_set.remove(&current);
-
-        if current == goal {
-            // println!("path found");
-            return reconstruct_path(came_from, current);
-        }
+        let mut nodes_expanded: usize = 0;
 
-        closed_set.insert(current);
+        while let Some(current_node) = scratch.open_set.pop() {
+            let current = current_node.position;
+            nodes_expanded += 1;
 
-        for &move_dir in &allowed_moves {
-            let neighbor_pos = current + move_dir;
+            if nodes_expanded > PATHFINDER_MAX_NODES {
+                return (
+                    reconstruct_path(&scratch.came_from, current),
+                    nodes_expanded,
+                );
+            }
 
-            if !is_standable_tile(tilemap, neighbor_pos) || closed_set.contains(&neighbor_pos) {
-                continue;
+            if current == goal {
+                return (
+                    reconstruct_path(&scratch.came_from, current),
+                    nodes_expanded,
+                );
             }
 
-            // TODO: Add terrain-based cost modifiers
-            // movement_cost = apply_terrain_modifier(movement_cost, neighbor_pos, tilemap);
-            // Examples:
-            // - Mud/sand: +50% cost
-            // - Ice: +100% cost
-            // - Designated high-traffic areas: -25% cost
-            // - Designated restricted areas: +500% cost
-            // - Etc
-
-            let movement_cost = match (
-                move_dir.x.abs() / ITILE_SIZE,
-                move_dir.y.abs() / ITILE_SIZE,
-                move_dir.z.abs() / ITILE_SIZE,
-            ) {
-                // 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, 1, 1) => 56,             // Diagonal + vertical climb
-
-                // TODO: Implement stairs and ramps for efficient vertical movement
-                // Stairs would reduce vertical costs significantly:
-                // (0, 0, 1) => 20 if has_stairs(current, neighbor_pos),     // Stairs: 2× horizontal cost
-                // (1, 0, 1) | (0, 1, 1) => 24 if has_stairs(current, neighbor_pos), // Stairs + horizontal
-                // (1, 1, 1) => 28 if has_stairs(current, neighbor_pos),     // Stairs + diagonal
-
-                // TODO: Implement ramps for even smoother vertical movement
-                // Ramps would be cheaper than stairs:
-                // (0, 0, 1) => 15 if has_ramp(current, neighbor_pos),       // Ramps: 1.5× horizontal cost
-                // (1, 0, 1) | (0, 1, 1) => 18 if has_ramp(current, neighbor_pos), // Ramps + horizontal
-                // (1, 1, 1) => 21 if has_ramp(current, neighbor_pos),       // Ramps + diagonal
-                _ => 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;
-
-                // Only add to open set if not already there
-                if !in_open_set.contains(&neighbor_pos) {
-                    let neighbor_node = PathNode {
+            scratch.closed_set.insert(current);
+
+            for &move_dir in &ALLOWED_MOVES {
+                let neighbor_pos = current + move_dir;
+
+                if !is_standable_tile(tilemap, neighbor_pos)
+                    || scratch.closed_set.contains(&neighbor_pos)
+                {
+                    continue;
+                }
+
+                let movement_cost = calculate_movement_cost(move_dir);
+                if movement_cost == 0 {
+                    continue;
+                }
+
+                let new_g = *scratch.g_scores.get(&current).unwrap_or(&i32::MAX) + movement_cost;
+
+                if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
+                    scratch.came_from.insert(neighbor_pos, current);
+                    scratch.g_scores.insert(neighbor_pos, new_g);
+                    let f = new_g + octile_distance_3d(neighbor_pos, goal);
+                    scratch.open_set.push(PathNode {
                         position: neighbor_pos,
                         f_score: f,
                         g_score: new_g,
-                    };
-                    open_set.push(neighbor_node);
-                    in_open_set.insert(neighbor_pos);
-                } else {
-                    let neighbor_node = PathNode {
+                    });
+                }
+            }
+        }
+
+        (Vec::new(), nodes_expanded)
+    })
+}
+
+pub fn calculate_provisional_path(
+    tilemap: &TileMap,
+    start: IVec3,
+    goal: IVec3,
+    node_limit: usize,
+) -> Vec<Vec3> {
+    let timer = Instant::now();
+
+    if !is_standable_tile(tilemap, start) {
+        LOCAL_FAILED_PATHS.with(|f| {
+            *f.borrow_mut() += 1;
+        });
+        return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)];
+    }
+
+    let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
+
+    let result = SCRATCHPAD.with(|s| {
+        let mut scratch = s.borrow_mut();
+        let capacity = ((estimated_tiles as usize).max(64)).min(4096);
+        scratch.clear_and_reserve(capacity);
+
+        let initial_h = octile_distance_3d(start, goal);
+        scratch.open_set.push(PathNode {
+            position: start,
+            f_score: initial_h,
+            g_score: 0,
+        });
+        scratch.g_scores.insert(start, 0);
+
+        let mut nodes_expanded: usize = 0;
+        let mut best_node = start;
+        let mut best_h = initial_h;
+
+        while let Some(current_node) = scratch.open_set.pop() {
+            let current = current_node.position;
+            nodes_expanded += 1;
+
+            let h = octile_distance_3d(current, goal);
+            if h < best_h {
+                best_h = h;
+                best_node = current;
+            }
+
+            if current == goal {
+                return (
+                    reconstruct_path(&scratch.came_from, current),
+                    nodes_expanded,
+                );
+            }
+
+            if nodes_expanded >= node_limit {
+                return (
+                    reconstruct_path(&scratch.came_from, best_node),
+                    nodes_expanded,
+                );
+            }
+
+            scratch.closed_set.insert(current);
+
+            for &move_dir in &ALLOWED_MOVES {
+                let neighbor_pos = current + move_dir;
+
+                if !is_standable_tile(tilemap, neighbor_pos)
+                    || scratch.closed_set.contains(&neighbor_pos)
+                {
+                    continue;
+                }
+
+                let movement_cost = calculate_movement_cost(move_dir);
+                if movement_cost == 0 {
+                    continue;
+                }
+
+                let new_g = *scratch.g_scores.get(&current).unwrap_or(&i32::MAX) + movement_cost;
+
+                if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
+                    scratch.came_from.insert(neighbor_pos, current);
+                    scratch.g_scores.insert(neighbor_pos, new_g);
+                    let f = new_g + octile_distance_3d(neighbor_pos, goal);
+                    scratch.open_set.push(PathNode {
                         position: neighbor_pos,
                         f_score: f,
                         g_score: new_g,
-                    };
-                    open_set.push(neighbor_node);
+                    });
                 }
             }
         }
-    }
 
-    Vec::new()
+        (
+            reconstruct_path(&scratch.came_from, best_node),
+            nodes_expanded,
+        )
+    });
+
+    let elapsed = timer.elapsed().as_micros();
+    LOCAL_PATH_TIMES.with(|t| {
+        t.borrow_mut().push(elapsed);
+    });
+    LOCAL_PATH_LENGTHS.with(|l| {
+        l.borrow_mut().push(result.0.len());
+    });
+    LOCAL_NODES_EXPANDED.with(|n| {
+        n.borrow_mut().push(result.1);
+    });
+
+    result.0
 }
 
-fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
-    let dx = (a.x - b.x).abs();
-    let dy = (a.y - b.y).abs();
-    let dz = (a.z - b.z).abs();
+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);
+        }
 
-    // Dwarf Fortress style costs
-    let cost_orthogonal = 10; // Horizontal orthogonal
-    let cost_diagonal = 14; // Horizontal diagonal (~√2 × 10)
-    let cost_climb = 50; // Raw vertical movement (climbing)
-
-    let mut diffs = [dx, dy, dz];
-    diffs.sort_unstable();
-    let dmin = diffs[0];
-    let dmax = diffs[2];
-
-    if dz == 0 {
-        // Pure 2D movement
-        let diagonal_moves = dmin / ITILE_SIZE;
-        let orthogonal_moves = (dmax - dmin) / ITILE_SIZE;
-        cost_diagonal * diagonal_moves + cost_orthogonal * orthogonal_moves
-    } else {
-        // Movement involves Z - assume raw climbing for now
-        // TODO: Modify this when stairs/ramps are implemented
-        let z_moves = dz / ITILE_SIZE;
-        let xy_distance = ((dx * dx + dy * dy) as f32).sqrt() as i32;
-        let remaining_2d_diagonal = (xy_distance.min(dz)) / ITILE_SIZE;
-        let remaining_2d_orthogonal =
-            (xy_distance - remaining_2d_diagonal * ITILE_SIZE) / ITILE_SIZE;
-
-        // Raw climbing cost + remaining 2D movement
-        cost_climb * z_moves
-            + cost_diagonal * remaining_2d_diagonal
-            + cost_orthogonal * remaining_2d_orthogonal
+        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
+            }
+        );
+
+        if let Err(e) = write_benchmark_csv(&bench, "pathfinding_benchmark_current.csv") {
+            eprintln!("Failed to write benchmark CSV: {}", e);
+        }
+        println!("=====================================\n");
     }
 }
 
-fn reconstruct_path(came_from: HashMap<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,
-    )];
+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={}µs median={}µs min={}µs max={}µs p95={}µs",
+        label,
+        times.len(),
+        avg,
+        median,
+        min,
+        max,
+        p95
+    );
+}
 
-    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;
+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)?;
     }
 
-    path.reverse();
-    path
+    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(())
 }
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..028c005 100644
--- a/src/world/tiles/tilemap.rs
+++ b/src/world/tiles/tilemap.rs
@@ -1,9 +1,197 @@
 use bevy::prelude::*;
-use bevy_platform::collections::hash_map::HashMap;
+use rustc_hash::FxHashMap;
 
-#[derive(Resource, Default, Clone)]
+use crate::constants::ITILE_SIZE;
+
+/// 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;
+        }
+    }
+}
+
+/// 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
+    }
+}
+
+/// Tile map using FxHashMap for fast lookups. No Arc wrapper - single-threaded access.
+#[derive(Resource, 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: FxHashMap<IVec3, FloorTileData>,
+    pub fixture_tiles: FxHashMap<IVec3, FixtureTileData>,
+    pub item_tiles: FxHashMap<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) {
+        self.floor_tiles.insert(pos, tile);
+    }
+
+    #[inline]
+    pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) {
+        self.fixture_tiles.insert(pos, tile);
+    }
+
+    #[inline]
+    pub fn insert_item(&mut self, pos: IVec3, entity_id: u32) {
+        self.item_tiles.entry(pos).or_default().push(entity_id);
+    }
+
+    #[inline]
+    pub fn remove_item(&mut self, pos: &IVec3) -> Option<Vec<u32>> {
+        self.item_tiles.remove(pos)
+    }
+
+    #[inline]
+    pub fn get_floor_mut(&mut self, pos: &IVec3) -> Option<&mut FloorTileData> {
+        self.floor_tiles.get_mut(pos)
+    }
 }
diff --git a/src/world/tiles/visibility.rs b/src/world/tiles/visibility.rs
index 5c9a041..c56ce8b 100644
--- a/src/world/tiles/visibility.rs
+++ b/src/world/tiles/visibility.rs
@@ -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;
             }
         }
     }
@@ -134,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 => {}
                         _ => {}
                     }
@@ -163,8 +163,8 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
         let camera_z_index = (check_pos.z / ITILE_SIZE) + z_below;
 
         if camera_z_index < 0 {
-            if let Some(&(_, _, _, vt, _, _)) = tilemap.floor_tiles.get(&check_pos) {
-                occluded = !vt;
+            if let Some(tile) = tilemap.floor_tiles.get(&check_pos) {
+                occluded = !tile.visibly_transparent();
             } else {
                 occluded = false;
             }
@@ -180,8 +180,8 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
             visible_range[z2 / 32] |= 1 << (z2 % 32);
         }
 
-        if let Some(&(_, _, _, vt, _, _)) = tilemap.floor_tiles.get(&check_pos) {
-            occluded = !vt;
+        if let Some(tile) = tilemap.floor_tiles.get(&check_pos) {
+            occluded = !tile.visibly_transparent();
         } else {
             occluded = false;
         }
diff --git a/verify_bench.py b/verify_bench.py
new file mode 100644
index 0000000..fea2e64
--- /dev/null
+++ b/verify_bench.py
@@ -0,0 +1,36 @@
+import csv
+import sys
+
+def analyze(filename):
+    durations = []
+    failed = 0
+    total = 0
+    stutters = 0
+    try:
+        with open(filename, 'r') as f:
+            reader = csv.reader(f)
+            next(reader) # skip header
+            for row in reader:
+                if not row or row[0].startswith('#'): continue
+                total += 1
+                try:
+                    d = int(row[1])
+                    durations.append(d)
+                    if d > 1000: stutters += 1
+                    if row[4] == 'false': failed += 1
+                except:
+                    pass
+        
+        durations.sort()
+        if not durations: return
+        p50 = durations[len(durations)//2]
+        p90 = durations[int(len(durations)*0.90)]
+        p99 = durations[int(len(durations)*0.99)]
+        max_d = durations[-1]
+        print(f"[{filename}]")
+        print(f"P50={p50} P90={p90} P99={p99} Max={max_d} Stutters={stutters} Failed={failed}/{total}")
+    except Exception as e:
+        print(f"Error reading {filename}: {e}")
+
+analyze("/home/popertots/bench/pathfinding_benchmark_baseline_release.csv")
+analyze("pathfinding_benchmark_current.csv")
