feat: soft entity collision, dead code removal, leaf standability fix
- Add TileOccupancy resource and rebuild_tile_occupancy system to track per-tile entity counts for soft collision avoidance - Add collision_delay to Ambulatory; entities try relative-left step on occupied tiles, wait 1 tick, then push through - Fix leaf canopy standability: leaves are now can_stand_in=true, can_stand_on=false (walkable, not standable-on) - Delete FloorTilePrefab / FixtureTilePrefab / FloorTile / FixtureTile / TileState (all fully dead — TileMap + ChunkData are sole truth) - Delete tile_spawns from TerrainBlob (populated but never consumed) - Delete leaf ghost entity spawn in forestry (orphaned invisible ECS entity) - Replace log prefab spawn with inline commands.spawn(Transform, Visibility) - Add TileMap::remove_fixture for future digging/explosion use - Skip collision avoidance when current tile has >2 entities (handles spawn cluster deadlock)
This commit is contained in:
Generated
+1
@@ -2432,6 +2432,7 @@ name = "dorf"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
|
"arrayvec",
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy_platform",
|
"bevy_platform",
|
||||||
"bevy_rand",
|
"bevy_rand",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ rustc-hash = "2.1.1"
|
|||||||
ahash = "0.8.12"
|
ahash = "0.8.12"
|
||||||
nohash-hasher = "0.2.0"
|
nohash-hasher = "0.2.0"
|
||||||
futures-lite = "2.6.1"
|
futures-lite = "2.6.1"
|
||||||
|
arrayvec = "0.7.6"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
image = "0.25.10"
|
image = "0.25.10"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
initial_chunk_radius = 15
|
initial_chunk_radius = 8
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
vsync = "mailbox"
|
vsync = "mailbox"
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
timestamp,entity_id,x,y,z,has_path,path_index,path_len,target_x,target_y,target_z
|
||||||
|
9.523,14600,-16.0,32.0,0.0,true,0,56,816.0,-816.0,33.0
|
||||||
|
9.523,14601,-0.0,32.0,0.0,true,0,60,640.0,-848.0,33.0
|
||||||
|
9.523,14602,-16.0,0.0,0.0,true,0,38,-608.0,-144.0,17.0
|
||||||
|
9.523,14603,0.0,-0.0,0.0,true,0,61,960.0,256.0,-15.0
|
||||||
|
9.523,14604,16.0,-32.0,16.0,true,0,10,752.0,48.0,17.0
|
||||||
|
9.523,14599,16.0,-16.0,16.0,true,0,46,-496.0,704.0,-15.0
|
||||||
|
9.523,14598,-32.0,16.0,0.0,true,0,62,-832.0,800.0,-31.0
|
||||||
|
9.523,14547,0.0,0.0,0.0,true,0,53,832.0,336.0,1.0
|
||||||
|
9.523,14548,0.0,-0.0,0.0,true,0,55,848.0,-752.0,33.0
|
||||||
|
9.523,14549,0.0,0.0,0.0,true,0,30,-96.0,464.0,-15.0
|
||||||
|
9.523,14550,0.0,-0.0,0.0,true,0,52,816.0,512.0,17.0
|
||||||
|
9.523,14551,0.0,-0.0,0.0,true,0,46,720.0,464.0,17.0
|
||||||
|
9.523,14552,-0.0,0.0,0.0,true,0,49,768.0,-128.0,33.0
|
||||||
|
9.523,14553,0.0,0.0,0.0,true,0,42,656.0,-400.0,49.0
|
||||||
|
9.523,14554,-0.0,0.0,0.0,true,0,34,-512.0,-416.0,49.0
|
||||||
|
9.523,14555,-0.0,-0.0,0.0,true,0,49,144.0,768.0,17.0
|
||||||
|
9.523,14597,16.0,16.0,0.0,true,0,58,-256.0,928.0,-15.0
|
||||||
|
9.523,14557,0.0,0.0,0.0,true,0,40,624.0,160.0,1.0
|
||||||
|
9.523,14558,-0.0,0.0,0.0,true,0,44,96.0,688.0,17.0
|
||||||
|
9.523,14559,0.0,-0.0,0.0,true,0,61,352.0,960.0,33.0
|
||||||
|
9.523,14560,0.0,0.0,0.0,true,0,46,-704.0,-352.0,33.0
|
||||||
|
9.523,14561,-0.0,-0.0,0.0,true,0,31,480.0,80.0,1.0
|
||||||
|
9.523,14562,0.0,0.0,0.0,true,0,46,-272.0,-720.0,65.0
|
||||||
|
9.523,14563,-0.0,-0.0,0.0,true,0,41,640.0,64.0,1.0
|
||||||
|
9.523,14564,-0.0,0.0,0.0,true,0,43,672.0,176.0,1.0
|
||||||
|
9.523,14565,0.0,0.0,0.0,true,0,54,-64.0,-848.0,49.0
|
||||||
|
9.523,14566,-0.0,-0.0,0.0,true,0,51,-640.0,-768.0,49.0
|
||||||
|
9.523,14596,-16.0,0.0,0.0,true,0,41,-512.0,-592.0,49.0
|
||||||
|
9.523,14568,0.0,0.0,0.0,true,0,32,224.0,-464.0,33.0
|
||||||
|
9.523,14569,0.0,-0.0,0.0,true,0,52,-656.0,784.0,-15.0
|
||||||
|
9.523,14570,-0.0,-0.0,0.0,true,0,33,-512.0,256.0,-31.0
|
||||||
|
9.523,14571,-0.0,0.0,0.0,true,0,49,176.0,-768.0,33.0
|
||||||
|
9.523,14572,-0.0,-0.0,0.0,true,0,40,-272.0,-624.0,49.0
|
||||||
|
9.523,14573,-0.0,0.0,0.0,true,0,61,960.0,336.0,1.0
|
||||||
|
9.523,14595,-16.0,32.0,0.0,true,0,54,288.0,-816.0,17.0
|
||||||
|
9.523,14575,0.0,-0.0,0.0,true,0,31,336.0,464.0,17.0
|
||||||
|
9.523,14576,0.0,0.0,0.0,true,0,63,992.0,-64.0,1.0
|
||||||
|
9.523,14577,-0.0,-0.0,0.0,true,0,47,-736.0,-112.0,17.0
|
||||||
|
9.523,14578,-0.0,-0.0,0.0,true,0,58,368.0,896.0,33.0
|
||||||
|
9.523,14579,0.0,0.0,0.0,true,0,40,-464.0,512.0,-15.0
|
||||||
|
9.523,14580,-0.0,-0.0,0.0,true,0,42,-288.0,-656.0,65.0
|
||||||
|
9.523,14581,0.0,-0.0,0.0,true,0,47,128.0,736.0,17.0
|
||||||
|
9.523,14582,0.0,0.0,0.0,true,0,47,736.0,-400.0,49.0
|
||||||
|
9.523,14583,-0.0,0.0,0.0,true,0,45,-368.0,-704.0,65.0
|
||||||
|
9.523,14594,0.0,0.0,0.0,true,0,40,-464.0,512.0,-15.0
|
||||||
|
9.523,14585,0.0,0.0,0.0,true,0,56,-880.0,336.0,-15.0
|
||||||
|
9.523,14586,0.0,-0.0,0.0,true,0,64,784.0,992.0,17.0
|
||||||
|
9.523,14587,0.0,0.0,0.0,true,0,64,896.0,-512.0,49.0
|
||||||
|
9.523,14588,0.0,-0.0,0.0,true,0,30,-464.0,288.0,-31.0
|
||||||
|
9.523,14592,0.0,0.0,0.0,true,0,50,48.0,-784.0,33.0
|
||||||
|
9.523,14590,0.0,0.0,0.0,true,0,44,688.0,-224.0,49.0
|
||||||
|
9.523,14591,-0.0,-0.0,0.0,true,0,46,-720.0,448.0,-31.0
|
||||||
|
9.523,14545,0.0,-0.0,0.0,true,0,20,80.0,-304.0,33.0
|
||||||
|
9.523,14546,0.0,0.0,0.0,true,0,5,368.0,576.0,17.0
|
||||||
|
9.523,14556,0.0,0.0,0.0,true,0,9,640.0,576.0,17.0
|
||||||
|
9.523,14567,0.0,-0.0,0.0,true,0,13,-528.0,-96.0,17.0
|
||||||
|
9.523,14574,0.0,0.0,0.0,true,0,13,-640.0,64.0,1.0
|
||||||
|
9.523,14584,0.0,0.0,0.0,true,0,13,-864.0,0.0,1.0
|
||||||
|
9.523,14589,-0.0,-0.0,0.0,true,0,19,-112.0,-288.0,33.0
|
||||||
|
9.523,14593,-0.0,-0.0,0.0,true,0,10,528.0,-32.0,17.0
|
||||||
|
+1423
File diff suppressed because it is too large
Load Diff
+2
-7
@@ -5,16 +5,11 @@ pub const ITILE_SIZE: i32 = TILE_SIZE as i32;
|
|||||||
pub const SEED: u32 = 420;
|
pub const SEED: u32 = 420;
|
||||||
|
|
||||||
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
|
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
|
||||||
pub const PATHFINDER_MAX_NODES: usize = 5000;
|
pub const PATHFINDER_MAX_NODES: usize = 15000;
|
||||||
pub const PATHFINDER_WAYPOINT_THRESHOLD_TILES: i32 = 100;
|
pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 256;
|
||||||
pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 64;
|
|
||||||
|
|
||||||
// Hierarchical pathfinding thresholds
|
// Hierarchical pathfinding thresholds
|
||||||
// Tier 1: Same/adjacent chunk -> sync A* (fast, ~87µs)
|
// Tier 1: Same/adjacent chunk -> sync A* (fast, ~87µs)
|
||||||
// Tier 2: 2-4 chunks away -> Provisional + full path via queue
|
// Tier 2: 2-4 chunks away -> Provisional + full path via queue
|
||||||
// Tier 3: >4 chunks away -> Hierarchical chunk-path + async segmented A*
|
// Tier 3: >4 chunks away -> Hierarchical chunk-path + async segmented A*
|
||||||
pub const PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS: i32 = 4;
|
pub const PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS: i32 = 4;
|
||||||
pub const PATHFINDER_ASYNC_NODE_BUDGET_PER_FRAME: usize = 2000;
|
|
||||||
|
|
||||||
// Snapshot bounds for async pathfinding (in chunks)
|
|
||||||
pub const PATHFINDER_SNAPSHOT_CHUNK_RADIUS: i32 = 2;
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::entities::shared_components::Ambulatory;
|
||||||
|
|
||||||
|
pub fn dump_entity_positions(
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
time: Res<Time>,
|
||||||
|
query: Query<(Entity, &Transform, Option<&Ambulatory>), With<Ambulatory>>,
|
||||||
|
) {
|
||||||
|
if !keys.just_pressed(KeyCode::Space) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = time.elapsed_secs();
|
||||||
|
let filename = format!("entity_dump_{:.2}.csv", timestamp);
|
||||||
|
|
||||||
|
let mut file = match OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&filename)
|
||||||
|
{
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create dump file: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"timestamp,entity_id,x,y,z,has_path,path_index,path_len,target_x,target_y,target_z"
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
for (entity, transform, ambulatory) in query.iter() {
|
||||||
|
let pos = transform.translation;
|
||||||
|
let (has_path, path_index, path_len, tx, ty, tz) = match ambulatory {
|
||||||
|
Some(a) => {
|
||||||
|
let path_len = a.current_path.as_ref().map(|p| p.len()).unwrap_or(0);
|
||||||
|
let has_path = path_len > 0;
|
||||||
|
let (tx, ty, tz) = match a.target {
|
||||||
|
Some(t) => (t.x, t.y, t.z),
|
||||||
|
None => (0.0, 0.0, 0.0),
|
||||||
|
};
|
||||||
|
(has_path, a.path_index, path_len, tx, ty, tz)
|
||||||
|
}
|
||||||
|
None => (false, 0, 0, 0.0, 0.0, 0.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
file,
|
||||||
|
"{:.3},{},{:.1},{:.1},{:.1},{},{},{},{:.1},{:.1},{:.1}",
|
||||||
|
timestamp,
|
||||||
|
entity.index(),
|
||||||
|
pos.x,
|
||||||
|
pos.y,
|
||||||
|
pos.z,
|
||||||
|
has_path,
|
||||||
|
path_index,
|
||||||
|
path_len,
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
tz,
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[DUMP] {} entities written to {}", count, filename);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod entity_dump;
|
||||||
@@ -30,6 +30,7 @@ impl Pig {
|
|||||||
path_index: 0,
|
path_index: 0,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
collision_delay: 0,
|
||||||
},
|
},
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("pig.png"),
|
image: asset_server.load("pig.png"),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ impl Rabbit {
|
|||||||
path_index: 0,
|
path_index: 0,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
collision_delay: 0,
|
||||||
},
|
},
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("rabbit.png"),
|
image: asset_server.load("rabbit.png"),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ impl Dorf {
|
|||||||
path_index: 0,
|
path_index: 0,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
collision_delay: 0,
|
||||||
},
|
},
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("dorf.png"),
|
image: asset_server.load("dorf.png"),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub struct Ambulatory {
|
|||||||
pub target: Option<Vec3>,
|
pub target: Option<Vec3>,
|
||||||
pub step_recovery: u32,
|
pub step_recovery: u32,
|
||||||
pub validation_cooldown: u8,
|
pub validation_cooldown: u8,
|
||||||
|
pub collision_delay: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Ambulatory {
|
impl Default for Ambulatory {
|
||||||
@@ -22,6 +23,7 @@ impl Default for Ambulatory {
|
|||||||
target: None,
|
target: None,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
collision_delay: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod occupancy;
|
||||||
pub mod pathfinding;
|
pub mod pathfinding;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
|
use crate::entities::shared_components::Ambulatory;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct TileOccupancy {
|
||||||
|
pub counts: FxHashMap<(i32, i32), u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileOccupancy {
|
||||||
|
#[inline]
|
||||||
|
pub fn count_at(&self, world_pos: Vec3) -> u8 {
|
||||||
|
let tx = (world_pos.x as i32) / ITILE_SIZE;
|
||||||
|
let ty = (world_pos.y as i32) / ITILE_SIZE;
|
||||||
|
*self.counts.get(&(tx, ty)).unwrap_or(&0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rebuild_tile_occupancy(
|
||||||
|
mut occupancy: ResMut<TileOccupancy>,
|
||||||
|
query: Query<&Transform, With<Ambulatory>>,
|
||||||
|
) {
|
||||||
|
occupancy.counts.clear();
|
||||||
|
for transform in query.iter() {
|
||||||
|
let tx = (transform.translation.x as i32) / ITILE_SIZE;
|
||||||
|
let ty = (transform.translation.y as i32) / ITILE_SIZE;
|
||||||
|
let count = occupancy.counts.entry((tx, ty)).or_insert(0);
|
||||||
|
if *count < 255 {
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,8 +61,9 @@ use std::{cell::RefCell, collections::BinaryHeap, collections::VecDeque, time::I
|
|||||||
|
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
||||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, TILE_SIZE,
|
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
||||||
};
|
};
|
||||||
|
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
use crate::world::{
|
use crate::world::{
|
||||||
chunks::CHUNK_SIZE,
|
chunks::CHUNK_SIZE,
|
||||||
@@ -215,9 +216,16 @@ impl Plugin for PathfindingPlugin {
|
|||||||
.insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
.insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
||||||
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
||||||
.insert_resource(PathRequestQueue::default())
|
.insert_resource(PathRequestQueue::default())
|
||||||
|
.init_resource::<TileOccupancy>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
(prepare_paths, update_wandering_targets, movement).chain(),
|
(
|
||||||
|
rebuild_tile_occupancy,
|
||||||
|
prepare_paths,
|
||||||
|
update_wandering_targets,
|
||||||
|
movement,
|
||||||
|
)
|
||||||
|
.chain(),
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
@@ -266,10 +274,11 @@ pub fn prepare_paths(
|
|||||||
ambulatory.path_index = 0;
|
ambulatory.path_index = 0;
|
||||||
} else if chunk_distance > PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS {
|
} else if chunk_distance > PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS {
|
||||||
let chunk_path = calculate_chunk_path(&chunk_map, start_chunk, goal_chunk);
|
let chunk_path = calculate_chunk_path(&chunk_map, start_chunk, goal_chunk);
|
||||||
|
let provisional_goal = goal;
|
||||||
let provisional = calculate_provisional_path(
|
let provisional = calculate_provisional_path(
|
||||||
&tilemap,
|
&tilemap,
|
||||||
start,
|
start,
|
||||||
goal,
|
provisional_goal,
|
||||||
PATHFINDER_PROVISIONAL_NODE_LIMIT,
|
PATHFINDER_PROVISIONAL_NODE_LIMIT,
|
||||||
);
|
);
|
||||||
if !provisional.is_empty() {
|
if !provisional.is_empty() {
|
||||||
@@ -296,9 +305,15 @@ pub fn prepare_paths(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||||
|
if path.len() <= 1 {
|
||||||
|
// Path failed — clear target so entity picks a new reachable one
|
||||||
|
ambulatory.target = None;
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
} else {
|
||||||
ambulatory.current_path = Some(path);
|
ambulatory.current_path = Some(path);
|
||||||
ambulatory.path_index = 0;
|
ambulatory.path_index = 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let provisional = calculate_provisional_path(
|
let provisional = calculate_provisional_path(
|
||||||
&tilemap,
|
&tilemap,
|
||||||
@@ -326,28 +341,29 @@ pub fn prepare_paths(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||||
|
if path.len() <= 1 {
|
||||||
|
// Path failed — clear target so entity picks a new reachable one
|
||||||
|
ambulatory.target = None;
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
} else {
|
||||||
ambulatory.current_path = Some(path);
|
ambulatory.current_path = Some(path);
|
||||||
ambulatory.path_index = 0;
|
ambulatory.path_index = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_path_queue(
|
pub fn process_path_queue(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut queue: ResMut<PathRequestQueue>,
|
mut queue: ResMut<PathRequestQueue>,
|
||||||
tilemap: Res<TileMap>,
|
tilemap: Res<TileMap>,
|
||||||
_chunk_map: Res<ChunkMap>,
|
chunk_map: Res<ChunkMap>,
|
||||||
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
|
||||||
mut query: Query<
|
mut query: Query<
|
||||||
(Entity, &mut Ambulatory, &Transform),
|
(Entity, &mut Ambulatory, &Transform),
|
||||||
With<crate::entities::shared_components::PendingPath>,
|
With<crate::entities::shared_components::PendingPath>,
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
let Ok(mut rng) = rng_q.single_mut() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut processed = 0;
|
let mut processed = 0;
|
||||||
while processed < MAX_PATHS_PER_FRAME {
|
while processed < MAX_PATHS_PER_FRAME {
|
||||||
if let Some(request) = queue.pending.pop_front() {
|
if let Some(request) = queue.pending.pop_front() {
|
||||||
@@ -360,13 +376,20 @@ pub fn process_path_queue(
|
|||||||
let current_chunk = world_to_chunk(actual_start);
|
let current_chunk = world_to_chunk(actual_start);
|
||||||
if let Some(next_chunk) = chunk_waypoints.iter().find(|&&c| c != current_chunk)
|
if let Some(next_chunk) = chunk_waypoints.iter().find(|&&c| c != current_chunk)
|
||||||
{
|
{
|
||||||
let waypoint = directional_chunk_waypoint(
|
match directional_chunk_waypoint(
|
||||||
actual_start,
|
actual_start,
|
||||||
*next_chunk,
|
*next_chunk,
|
||||||
request.goal,
|
request.goal,
|
||||||
&tilemap,
|
&tilemap,
|
||||||
&mut rng,
|
&chunk_map,
|
||||||
);
|
) {
|
||||||
|
None => {
|
||||||
|
// Chunk unloaded or no standable tiles — abandon this path,
|
||||||
|
// entity will retarget via prepare_paths next frame
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
ambulatory.target = None;
|
||||||
|
}
|
||||||
|
Some(waypoint) => {
|
||||||
let segment_path =
|
let segment_path =
|
||||||
calculate_path_benchmarked(&tilemap, actual_start, waypoint);
|
calculate_path_benchmarked(&tilemap, actual_start, waypoint);
|
||||||
if !segment_path.is_empty() {
|
if !segment_path.is_empty() {
|
||||||
@@ -374,6 +397,8 @@ pub fn process_path_queue(
|
|||||||
ambulatory.path_index = 0;
|
ambulatory.path_index = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let full_path =
|
let full_path =
|
||||||
calculate_path_benchmarked(&tilemap, actual_start, request.goal);
|
calculate_path_benchmarked(&tilemap, actual_start, request.goal);
|
||||||
@@ -447,18 +472,57 @@ pub fn update_wandering_targets(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (mut ambulatory, _) in query.iter_mut() {
|
for (mut ambulatory, transform) in query.iter_mut() {
|
||||||
if ambulatory.target.is_none()
|
if ambulatory.target.is_none() {
|
||||||
|| (ambulatory.current_path.is_some()
|
// Only target interior chunks — exclude boundary chunks that
|
||||||
&& ambulatory.path_index >= ambulatory.current_path.as_ref().unwrap().len())
|
// have unloaded neighbours, which strand entities at world edges.
|
||||||
|
let current_chunk = world_to_chunk(transform.translation.as_ivec3());
|
||||||
|
|
||||||
|
// Reservoir sampling - pick one interior chunk with zero allocation
|
||||||
|
let mut chosen: Option<IVec2> = None;
|
||||||
|
let mut count = 0usize;
|
||||||
|
for &chunk in chunk_map.loaded_chunks.keys() {
|
||||||
|
let dx = (chunk.x - current_chunk.x).abs();
|
||||||
|
let dy = (chunk.y - current_chunk.y).abs();
|
||||||
|
if dx <= 2 && dy <= 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !chunk_map
|
||||||
|
.loaded_chunks
|
||||||
|
.contains_key(&IVec2::new(chunk.x + 1, chunk.y))
|
||||||
{
|
{
|
||||||
let loaded_chunks: Vec<&IVec2> = chunk_map.loaded_chunks.keys().collect();
|
continue;
|
||||||
if !loaded_chunks.is_empty() {
|
}
|
||||||
let random_index = rng.random_range(0..loaded_chunks.len());
|
if !chunk_map
|
||||||
if let Some(&chunk_pos) = loaded_chunks.get(random_index) {
|
.loaded_chunks
|
||||||
|
.contains_key(&IVec2::new(chunk.x - 1, chunk.y))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !chunk_map
|
||||||
|
.loaded_chunks
|
||||||
|
.contains_key(&IVec2::new(chunk.x, chunk.y + 1))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !chunk_map
|
||||||
|
.loaded_chunks
|
||||||
|
.contains_key(&IVec2::new(chunk.x, chunk.y - 1))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
if rng.random_range(0..count) == 0 {
|
||||||
|
chosen = Some(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to any chunk if no interior chunks found (small map / startup)
|
||||||
|
let target_chunk = chosen.or_else(|| chunk_map.loaded_chunks.keys().next().copied());
|
||||||
|
|
||||||
|
if let Some(chunk_pos) = target_chunk {
|
||||||
let chunk_x = chunk_pos.x * CHUNK_SIZE;
|
let chunk_x = chunk_pos.x * CHUNK_SIZE;
|
||||||
let chunk_y = chunk_pos.y * CHUNK_SIZE;
|
let chunk_y = chunk_pos.y * CHUNK_SIZE;
|
||||||
|
|
||||||
let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
|
let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
|
||||||
let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
|
let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
|
||||||
|
|
||||||
@@ -484,9 +548,12 @@ pub fn update_wandering_targets(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Res<TileMap>) {
|
pub fn movement(
|
||||||
|
mut query: Query<(&mut Ambulatory, &mut Transform)>,
|
||||||
|
tilemap: Res<TileMap>,
|
||||||
|
occupancy: Res<TileOccupancy>,
|
||||||
|
) {
|
||||||
query
|
query
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.for_each(|(mut ambulatory, mut transform)| {
|
.for_each(|(mut ambulatory, mut transform)| {
|
||||||
@@ -528,6 +595,42 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
|
|||||||
if let Some(path) = &ambulatory.current_path {
|
if let Some(path) = &ambulatory.current_path {
|
||||||
if ambulatory.path_index < path.len() {
|
if ambulatory.path_index < path.len() {
|
||||||
let next_point = path[ambulatory.path_index];
|
let next_point = path[ambulatory.path_index];
|
||||||
|
|
||||||
|
if ambulatory.collision_delay > 0 {
|
||||||
|
ambulatory.collision_delay -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_crowded = occupancy.count_at(transform.translation) > 2;
|
||||||
|
let occupied = !current_crowded && occupancy.count_at(next_point) > 0;
|
||||||
|
if occupied {
|
||||||
|
let move_dir = next_point - transform.translation;
|
||||||
|
if move_dir.length_squared() > 0.0 {
|
||||||
|
let left_dir =
|
||||||
|
Vec3::new(-move_dir.y, move_dir.x, 0.0).normalize() * TILE_SIZE;
|
||||||
|
let left_raw = transform.translation + left_dir;
|
||||||
|
let left_point = Vec3::new(
|
||||||
|
(left_raw.x / TILE_SIZE).round() * TILE_SIZE,
|
||||||
|
(left_raw.y / TILE_SIZE).round() * TILE_SIZE,
|
||||||
|
transform.translation.z,
|
||||||
|
);
|
||||||
|
let left_free = tilemap.is_standable(left_point.as_ivec3())
|
||||||
|
&& occupancy.count_at(left_point) == 0;
|
||||||
|
|
||||||
|
if left_free {
|
||||||
|
if left_dir.x > 0.0 {
|
||||||
|
transform.scale.x = PIXEL_RATIO;
|
||||||
|
} else if left_dir.x < 0.0 {
|
||||||
|
transform.scale.x = -PIXEL_RATIO;
|
||||||
|
}
|
||||||
|
transform.translation = left_point;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ambulatory.collision_delay = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let direction = (next_point - transform.translation).normalize();
|
let direction = (next_point - transform.translation).normalize();
|
||||||
transform.translation = next_point;
|
transform.translation = next_point;
|
||||||
|
|
||||||
@@ -542,9 +645,13 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
|
if let Some(target) = ambulatory.target {
|
||||||
|
if transform.translation.distance(target) < TILE_SIZE * 2.0 {
|
||||||
ambulatory.target = None;
|
ambulatory.target = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,44 +682,28 @@ fn directional_chunk_waypoint(
|
|||||||
next_chunk: IVec2,
|
next_chunk: IVec2,
|
||||||
goal: IVec3,
|
goal: IVec3,
|
||||||
tilemap: &TileMap,
|
tilemap: &TileMap,
|
||||||
_rng: &mut WyRand,
|
chunk_map: &ChunkMap,
|
||||||
) -> IVec3 {
|
) -> Option<IVec3> {
|
||||||
|
if !chunk_map.loaded_chunks.contains_key(&next_chunk) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let cx = next_chunk.x * CHUNK_SIZE;
|
let cx = next_chunk.x * CHUNK_SIZE;
|
||||||
let cy = next_chunk.y * CHUNK_SIZE;
|
let cy = next_chunk.y * CHUNK_SIZE;
|
||||||
|
|
||||||
// Determine entry edge of the destination chunk.
|
|
||||||
// We work in ITILE units so 1 step = one tile of movement.
|
|
||||||
let cur_tile = current_pos / ITILE_SIZE;
|
let cur_tile = current_pos / ITILE_SIZE;
|
||||||
let goal_tile = goal / ITILE_SIZE;
|
let goal_tile = goal / ITILE_SIZE;
|
||||||
let dir = goal_tile - cur_tile;
|
let dir = goal_tile - cur_tile;
|
||||||
|
let straight = goal - current_pos;
|
||||||
|
|
||||||
// Edge: X-dominant → top/bottom; Y-dominant → left/right; tie → use Y
|
let mut candidates = arrayvec::ArrayVec::<IVec3, 16>::new();
|
||||||
let (fixed_axis, fixed_tile, var_min, var_max): (bool, i32, i32, i32) =
|
|
||||||
if dir.x.abs() >= dir.y.abs() {
|
|
||||||
// Moving east (+) or west (-)
|
|
||||||
if dir.x >= 0 {
|
|
||||||
(true, cx + CHUNK_SIZE - 1, cy, cy + CHUNK_SIZE - 1) // east edge
|
|
||||||
} else {
|
|
||||||
(true, cx, cy, cy + CHUNK_SIZE - 1) // west edge
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Moving north (+) or south (-)
|
|
||||||
if dir.y >= 0 {
|
|
||||||
(false, cy + CHUNK_SIZE - 1, cx, cx + CHUNK_SIZE - 1) // north edge
|
|
||||||
} else {
|
|
||||||
(false, cy, cx, cx + CHUNK_SIZE - 1) // south edge
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Collect up to 8 standable edge tiles.
|
let mut scan_edge = |fixed_axis: bool, fixed_tile: i32, var_min: i32, var_max: i32| {
|
||||||
let mut candidates: Vec<IVec3> = Vec::with_capacity(8);
|
|
||||||
let var_range = var_max - var_min;
|
let var_range = var_max - var_min;
|
||||||
let step = if var_range <= 0 {
|
let step = if var_range <= 0 {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
(var_range / 7).max(1)
|
(var_range / 7).max(1)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut var = var_min;
|
let mut var = var_min;
|
||||||
while var <= var_max {
|
while var <= var_max {
|
||||||
let (tile_x, tile_y) = if fixed_axis {
|
let (tile_x, tile_y) = if fixed_axis {
|
||||||
@@ -620,8 +711,6 @@ fn directional_chunk_waypoint(
|
|||||||
} else {
|
} else {
|
||||||
(var, fixed_tile)
|
(var, fixed_tile)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Match the z-level of the current tile so we don't jump z-levels here.
|
|
||||||
let world_pos = IVec3::new(
|
let world_pos = IVec3::new(
|
||||||
tile_x * ITILE_SIZE,
|
tile_x * ITILE_SIZE,
|
||||||
tile_y * ITILE_SIZE,
|
tile_y * ITILE_SIZE,
|
||||||
@@ -629,39 +718,94 @@ fn directional_chunk_waypoint(
|
|||||||
);
|
);
|
||||||
if tilemap.is_standable(world_pos) {
|
if tilemap.is_standable(world_pos) {
|
||||||
candidates.push(world_pos);
|
candidates.push(world_pos);
|
||||||
if candidates.len() >= 8 {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var += step;
|
var += step;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let near_diagonal = (dir.x.abs() - dir.y.abs()).abs() < CHUNK_SIZE / 2;
|
||||||
|
|
||||||
|
if dir.x.abs() >= dir.y.abs() {
|
||||||
|
scan_edge(
|
||||||
|
true,
|
||||||
|
if dir.x >= 0 { cx } else { cx + CHUNK_SIZE - 1 },
|
||||||
|
cy,
|
||||||
|
cy + CHUNK_SIZE - 1,
|
||||||
|
);
|
||||||
|
if near_diagonal {
|
||||||
|
scan_edge(
|
||||||
|
false,
|
||||||
|
if dir.y >= 0 { cy } else { cy + CHUNK_SIZE - 1 },
|
||||||
|
cx,
|
||||||
|
cx + CHUNK_SIZE - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scan_edge(
|
||||||
|
false,
|
||||||
|
if dir.y >= 0 { cy } else { cy + CHUNK_SIZE - 1 },
|
||||||
|
cx,
|
||||||
|
cx + CHUNK_SIZE - 1,
|
||||||
|
);
|
||||||
|
if near_diagonal {
|
||||||
|
scan_edge(
|
||||||
|
true,
|
||||||
|
if dir.x >= 0 { cx } else { cx + CHUNK_SIZE - 1 },
|
||||||
|
cy,
|
||||||
|
cy + CHUNK_SIZE - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
// Fallback: chunk centre (the old behaviour)
|
let centre = IVec3::new(
|
||||||
IVec3::new(
|
|
||||||
(cx + CHUNK_SIZE / 2) * ITILE_SIZE,
|
(cx + CHUNK_SIZE / 2) * ITILE_SIZE,
|
||||||
(cy + CHUNK_SIZE / 2) * ITILE_SIZE,
|
(cy + CHUNK_SIZE / 2) * ITILE_SIZE,
|
||||||
cur_tile.z * ITILE_SIZE,
|
cur_tile.z * ITILE_SIZE,
|
||||||
)
|
);
|
||||||
|
if tilemap.is_standable(centre) {
|
||||||
|
return Some(centre);
|
||||||
|
}
|
||||||
|
for lx in 0..CHUNK_SIZE {
|
||||||
|
for ly in 0..CHUNK_SIZE {
|
||||||
|
let p = IVec3::new(
|
||||||
|
(cx + lx) * ITILE_SIZE,
|
||||||
|
(cy + ly) * ITILE_SIZE,
|
||||||
|
cur_tile.z * ITILE_SIZE,
|
||||||
|
);
|
||||||
|
if tilemap.is_standable(p) {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
// Pick the candidate closest to the straight-line projection.
|
// Find the candidate most aligned with the straight-line direction
|
||||||
let straight = goal - current_pos;
|
let best = candidates
|
||||||
let _straight_len_sq =
|
|
||||||
straight.x * straight.x + straight.y * straight.y + straight.z * straight.z;
|
|
||||||
|
|
||||||
candidates
|
|
||||||
.iter()
|
.iter()
|
||||||
.min_by(|&&a, &&b| {
|
.enumerate()
|
||||||
|
.max_by(|(_, &a), (_, &b)| {
|
||||||
let da = a - current_pos;
|
let da = a - current_pos;
|
||||||
let db = b - current_pos;
|
let db = b - current_pos;
|
||||||
// Dot product with straight direction: higher = more aligned
|
(da.x * straight.x + da.y * straight.y + da.z * straight.z)
|
||||||
let proj_a = (da.x * straight.x + da.y * straight.y + da.z * straight.z) as i64;
|
.cmp(&(db.x * straight.x + db.y * straight.y + db.z * straight.z))
|
||||||
let proj_b = (db.x * straight.x + db.y * straight.y + db.z * straight.z) as i64;
|
|
||||||
proj_b.cmp(&proj_a)
|
|
||||||
})
|
})
|
||||||
.copied()
|
.map(|(idx, _)| idx)
|
||||||
.unwrap()
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Spread entities using both their start position AND goal position as entropy.
|
||||||
|
// current_pos varies per entity (each is at a different world position).
|
||||||
|
// goal varies per entity (each has a different random wander target).
|
||||||
|
// Together they produce unique spread values for entities even when
|
||||||
|
// heading through the same chunk, without needing to pass entity ID.
|
||||||
|
let entropy = (cur_tile.x.unsigned_abs() as usize)
|
||||||
|
.wrapping_mul(1619)
|
||||||
|
.wrapping_add((cur_tile.y.unsigned_abs() as usize).wrapping_mul(31337))
|
||||||
|
.wrapping_add((goal_tile.x.unsigned_abs() as usize).wrapping_mul(6271))
|
||||||
|
.wrapping_add((goal_tile.y.unsigned_abs() as usize).wrapping_mul(2053));
|
||||||
|
let spread = entropy % candidates.len();
|
||||||
|
let idx = (best + spread) % candidates.len();
|
||||||
|
Some(candidates[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,103 +871,6 @@ fn reconstruct_path(came_from: &FxHashMap<IVec3, IVec3>, mut current: IVec3) ->
|
|||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_of_sight(tilemap: &TileMap, from: IVec3, to: IVec3) -> bool {
|
|
||||||
let dx = (to.x - from.x).abs();
|
|
||||||
let dy = (to.y - from.y).abs();
|
|
||||||
let dz = (to.z - from.z).abs();
|
|
||||||
let sx = if from.x < to.x {
|
|
||||||
1
|
|
||||||
} else if from.x > to.x {
|
|
||||||
-1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let sy = if from.y < to.y {
|
|
||||||
1
|
|
||||||
} else if from.y > to.y {
|
|
||||||
-1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let sz = if from.z < to.z {
|
|
||||||
1
|
|
||||||
} else if from.z > to.z {
|
|
||||||
-1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let mut x = from.x;
|
|
||||||
let mut y = from.y;
|
|
||||||
let mut z = from.z;
|
|
||||||
let mut err_x = dx / 2;
|
|
||||||
let mut err_y = dy / 2;
|
|
||||||
let mut err_z = dz / 2;
|
|
||||||
let mut err = err_x + err_y + err_z;
|
|
||||||
let steps = (dx + dy + dz) as i32;
|
|
||||||
for _ in 0..steps {
|
|
||||||
if !is_standable_tile(tilemap, IVec3::new(x, y, z)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if err_x < 0 {
|
|
||||||
x += sx;
|
|
||||||
err_x += dy + dz;
|
|
||||||
} else if err_x >= dx {
|
|
||||||
x -= sx;
|
|
||||||
err_x -= dy + dz;
|
|
||||||
}
|
|
||||||
if err_y < 0 {
|
|
||||||
y += sy;
|
|
||||||
err_y += dx + dz;
|
|
||||||
} else if err_y >= dy {
|
|
||||||
y -= sy;
|
|
||||||
err_y -= dx + dz;
|
|
||||||
}
|
|
||||||
if err_z < 0 {
|
|
||||||
z += sz;
|
|
||||||
err_z += dx + dy;
|
|
||||||
} else if err_z >= dz {
|
|
||||||
z -= sz;
|
|
||||||
err_z -= dx + dy;
|
|
||||||
}
|
|
||||||
if x == to.x && y == to.y && z == to.z {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
err = err_x + err_y + err_z;
|
|
||||||
}
|
|
||||||
is_standable_tile(tilemap, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn theta_line_cost(from: IVec3, to: IVec3) -> i32 {
|
|
||||||
let dx = (to.x - from.x).abs() / ITILE_SIZE;
|
|
||||||
let dy = (to.y - from.y).abs() / ITILE_SIZE;
|
|
||||||
let dz = (to.z - from.z).abs() / ITILE_SIZE;
|
|
||||||
let dmax = dx.max(dy).max(dz);
|
|
||||||
let dmid = dx.min(dy).min(dz);
|
|
||||||
let dmin = dx + dy + dz - dmax - dmid;
|
|
||||||
(10 * dmax + 4 * dmid + dmin).max(10)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn smooth_path(path: &[Vec3], tilemap: &TileMap) -> Vec<Vec3> {
|
|
||||||
if path.len() < 3 {
|
|
||||||
return path.to_vec();
|
|
||||||
}
|
|
||||||
let mut result = Vec::with_capacity(path.len());
|
|
||||||
result.push(path[0]);
|
|
||||||
let mut i = 0;
|
|
||||||
while i < path.len() - 1 {
|
|
||||||
let mut j = path.len() - 1;
|
|
||||||
while j > i + 1 {
|
|
||||||
if line_of_sight(tilemap, path[i].as_ivec3(), path[j].as_ivec3()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
result.push(path[j]);
|
|
||||||
i = j;
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate_path_benchmarked(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
|
pub fn calculate_path_benchmarked(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
|
||||||
let timer = Instant::now();
|
let timer = Instant::now();
|
||||||
|
|
||||||
@@ -882,10 +929,7 @@ fn calculate_path_with_scratchpad(
|
|||||||
nodes_expanded += 1;
|
nodes_expanded += 1;
|
||||||
|
|
||||||
if nodes_expanded > PATHFINDER_MAX_NODES {
|
if nodes_expanded > PATHFINDER_MAX_NODES {
|
||||||
return (
|
return (Vec::new(), nodes_expanded); // force retarget
|
||||||
reconstruct_path(&scratch.came_from, current),
|
|
||||||
nodes_expanded,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if current == goal {
|
if current == goal {
|
||||||
@@ -899,22 +943,6 @@ fn calculate_path_with_scratchpad(
|
|||||||
|
|
||||||
let current_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX);
|
let current_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX);
|
||||||
|
|
||||||
if let Some(&parent) = scratch.came_from.get(¤t) {
|
|
||||||
if scratch.came_from.contains_key(&parent) {
|
|
||||||
let gp = *scratch.came_from.get(&parent).unwrap();
|
|
||||||
if line_of_sight(tilemap, gp, current) {
|
|
||||||
let gp_g = *scratch.g_scores.get(&gp).unwrap_or(&i32::MAX);
|
|
||||||
let via_gp = gp_g + theta_line_cost(gp, current);
|
|
||||||
if via_gp < current_g {
|
|
||||||
scratch.came_from.insert(current, gp);
|
|
||||||
scratch.g_scores.insert(current, via_gp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_g_updated = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX);
|
|
||||||
|
|
||||||
for &move_dir in &ALLOWED_MOVES {
|
for &move_dir in &ALLOWED_MOVES {
|
||||||
let neighbor_pos = current + move_dir;
|
let neighbor_pos = current + move_dir;
|
||||||
|
|
||||||
@@ -930,36 +958,16 @@ fn calculate_path_with_scratchpad(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let via_current_g = current_g_updated + movement_cost;
|
let new_g = current_g + movement_cost;
|
||||||
|
|
||||||
if let Some(&neighbor_parent) = scratch.came_from.get(&neighbor_pos) {
|
if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||||||
if scratch.came_from.contains_key(&neighbor_parent) {
|
|
||||||
let np_g = *scratch.g_scores.get(&neighbor_parent).unwrap_or(&i32::MAX);
|
|
||||||
if line_of_sight(tilemap, neighbor_parent, neighbor_pos) {
|
|
||||||
let via_np_gp = np_g + theta_line_cost(neighbor_parent, neighbor_pos);
|
|
||||||
if via_np_gp < via_current_g {
|
|
||||||
scratch.came_from.insert(neighbor_pos, neighbor_parent);
|
|
||||||
scratch.g_scores.insert(neighbor_pos, via_np_gp);
|
|
||||||
let f = via_np_gp + octile_distance_3d(neighbor_pos, goal);
|
|
||||||
scratch.open_set.push(PathNode {
|
|
||||||
position: neighbor_pos,
|
|
||||||
f_score: f,
|
|
||||||
g_score: via_np_gp,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if via_current_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
|
||||||
scratch.came_from.insert(neighbor_pos, current);
|
scratch.came_from.insert(neighbor_pos, current);
|
||||||
scratch.g_scores.insert(neighbor_pos, via_current_g);
|
scratch.g_scores.insert(neighbor_pos, new_g);
|
||||||
let f = via_current_g + octile_distance_3d(neighbor_pos, goal);
|
let f = new_g + octile_distance_3d(neighbor_pos, goal);
|
||||||
scratch.open_set.push(PathNode {
|
scratch.open_set.push(PathNode {
|
||||||
position: neighbor_pos,
|
position: neighbor_pos,
|
||||||
f_score: f,
|
f_score: f,
|
||||||
g_score: via_current_g,
|
g_score: new_g,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1021,10 +1029,7 @@ pub fn calculate_provisional_path(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nodes_expanded >= node_limit {
|
if nodes_expanded >= node_limit {
|
||||||
return (
|
return (Vec::new(), nodes_expanded);
|
||||||
reconstruct_path(&scratch.came_from, best_node),
|
|
||||||
nodes_expanded,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scratch.closed_set.insert(current);
|
scratch.closed_set.insert(current);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod camera;
|
|||||||
mod config;
|
mod config;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
mod debug;
|
||||||
mod entities;
|
mod entities;
|
||||||
mod game;
|
mod game;
|
||||||
mod system_info;
|
mod system_info;
|
||||||
@@ -67,5 +68,6 @@ fn main() {
|
|||||||
Update,
|
Update,
|
||||||
item_tile_management_system.after(initialize_item_rotation_state),
|
item_tile_management_system.after(initialize_item_rotation_state),
|
||||||
)
|
)
|
||||||
|
.add_systems(Update, debug::entity_dump::dump_entity_positions)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ use crate::{
|
|||||||
constants::{SEED, TILE_SIZE},
|
constants::{SEED, TILE_SIZE},
|
||||||
world::{
|
world::{
|
||||||
tiles::{FixtureTileData, TileMap},
|
tiles::{FixtureTileData, TileMap},
|
||||||
ChunkForrestryEvent, ChunkMap, ChunkOwner, FixtureTilePrefab, TextureIDs, Textures,
|
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
|
||||||
VisibleGameEntity,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,8 +71,12 @@ pub fn generate_chunk_forrestry(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let trunk_entity =
|
let trunk_entity = commands
|
||||||
FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
|
.spawn((
|
||||||
|
Transform::from_translation(trunk_pos),
|
||||||
|
Visibility::Hidden,
|
||||||
|
))
|
||||||
|
.id();
|
||||||
tree_positions.push(trunk_pos);
|
tree_positions.push(trunk_pos);
|
||||||
commands
|
commands
|
||||||
.entity(trunk_entity)
|
.entity(trunk_entity)
|
||||||
@@ -136,7 +139,6 @@ pub fn generate_chunk_forrestry(
|
|||||||
* (1.0 + (rng.random::<f32>() * 0.35 - 0.1));
|
* (1.0 + (rng.random::<f32>() * 0.35 - 0.1));
|
||||||
|
|
||||||
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
|
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
|
||||||
FixtureTilePrefab::leaves(pos).spawn(&mut commands);
|
|
||||||
if let Some(texture_id) = texture_ids.refs.get(&500005)
|
if let Some(texture_id) = texture_ids.refs.get(&500005)
|
||||||
{
|
{
|
||||||
if let Some(texture) =
|
if let Some(texture) =
|
||||||
@@ -166,7 +168,7 @@ pub fn generate_chunk_forrestry(
|
|||||||
(
|
(
|
||||||
ivec,
|
ivec,
|
||||||
FixtureTileData::new(
|
FixtureTileData::new(
|
||||||
5, false, true, [0; 8],
|
5, true, false, [0; 8],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
constants::{SEED, TILE_SIZE},
|
constants::{SEED, TILE_SIZE},
|
||||||
world::{
|
world::{
|
||||||
tiles::{ChunkData, FloorTileData, TileMap},
|
tiles::{ChunkData, FloorTileData, TileMap},
|
||||||
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent,
|
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
|
||||||
CHUNK_SIZE, Z_ABOVE, Z_BELOW,
|
CHUNK_SIZE, Z_ABOVE, Z_BELOW,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,6 @@ pub struct TerrainBlob {
|
|||||||
pub chunk_data: ChunkData,
|
pub chunk_data: ChunkData,
|
||||||
pub tile_updates: Vec<(IVec3, FloorTileData)>,
|
pub tile_updates: Vec<(IVec3, FloorTileData)>,
|
||||||
pub surface_positions: Vec<(Vec3, String)>,
|
pub surface_positions: Vec<(Vec3, String)>,
|
||||||
pub tile_spawns: Vec<(Vec3, FloorTilePrefab)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
|
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
|
||||||
@@ -75,7 +74,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
let mut chunk_data = ChunkData::new(chunk_pos);
|
let mut chunk_data = ChunkData::new(chunk_pos);
|
||||||
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
|
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
|
||||||
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
|
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
|
||||||
let mut tile_spawns: Vec<(Vec3, FloorTilePrefab)> = Vec::new();
|
|
||||||
|
|
||||||
for local_y in 0..CHUNK_SIZE {
|
for local_y in 0..CHUNK_SIZE {
|
||||||
for local_x in 0..CHUNK_SIZE {
|
for local_x in 0..CHUNK_SIZE {
|
||||||
@@ -107,7 +105,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
]);
|
]);
|
||||||
if cave_value < -0.75 {
|
if cave_value < -0.75 {
|
||||||
let tile = registry.floor("air");
|
let tile = registry.floor("air");
|
||||||
tile_spawns.push((position, FloorTilePrefab::air(position)));
|
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
FloorTileData::new(
|
FloorTileData::new(
|
||||||
@@ -130,7 +127,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
);
|
);
|
||||||
} else if cave_value < 0.8 {
|
} else if cave_value < 0.8 {
|
||||||
let tile = registry.floor("rock");
|
let tile = registry.floor("rock");
|
||||||
tile_spawns.push((position, FloorTilePrefab::rock(position)));
|
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
FloorTileData::new(
|
FloorTileData::new(
|
||||||
@@ -153,7 +149,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let tile = registry.floor("dirt");
|
let tile = registry.floor("dirt");
|
||||||
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
|
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
FloorTileData::new(
|
FloorTileData::new(
|
||||||
@@ -178,7 +173,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
} else if noise_position.z > position.z {
|
} else if noise_position.z > position.z {
|
||||||
if surface_height <= position.z + TILE_SIZE {
|
if surface_height <= position.z + TILE_SIZE {
|
||||||
let tile = registry.floor("grass");
|
let tile = registry.floor("grass");
|
||||||
tile_spawns.push((position, FloorTilePrefab::grass(position)));
|
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
FloorTileData::new(
|
FloorTileData::new(
|
||||||
@@ -202,7 +196,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
surface_positions.push((position, "grass".to_string()));
|
surface_positions.push((position, "grass".to_string()));
|
||||||
} else {
|
} else {
|
||||||
let tile = registry.floor("dirt");
|
let tile = registry.floor("dirt");
|
||||||
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
|
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
FloorTileData::new(
|
FloorTileData::new(
|
||||||
@@ -226,7 +219,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let tile = registry.floor("air");
|
let tile = registry.floor("air");
|
||||||
tile_spawns.push((position, FloorTilePrefab::air(position)));
|
|
||||||
tile_updates.push((
|
tile_updates.push((
|
||||||
pos_ivec,
|
pos_ivec,
|
||||||
FloorTileData::new(
|
FloorTileData::new(
|
||||||
@@ -257,7 +249,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
chunk_data,
|
chunk_data,
|
||||||
tile_updates,
|
tile_updates,
|
||||||
surface_positions,
|
surface_positions,
|
||||||
tile_spawns,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-7
@@ -2,8 +2,8 @@ use crate::{
|
|||||||
camera,
|
camera,
|
||||||
config::{GameConfig, VsyncMode},
|
config::{GameConfig, VsyncMode},
|
||||||
world::generation::{
|
world::generation::{
|
||||||
generate_chunk_fauna, generate_chunk_foliage, generate_chunk_forrestry,
|
apply_terrain_blobs, generate_chunk_fauna, generate_chunk_foliage,
|
||||||
generate_chunk_weathering_and_precipitation, apply_terrain_blobs, spawn_terrain_tasks,
|
generate_chunk_forrestry, generate_chunk_weathering_and_precipitation, spawn_terrain_tasks,
|
||||||
TerrainBlobStorage,
|
TerrainBlobStorage,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -16,12 +16,9 @@ pub mod tiles;
|
|||||||
|
|
||||||
pub use chunks::management::*;
|
pub use chunks::management::*;
|
||||||
pub use textures::management::*;
|
pub use textures::management::*;
|
||||||
pub use tiles::{prefabs::*, rendering::*, visibility::*};
|
pub use tiles::{rendering::*, visibility::*};
|
||||||
|
|
||||||
pub fn apply_vsync_setting(
|
pub fn apply_vsync_setting(config: Res<GameConfig>, mut windows: Query<&mut Window>) {
|
||||||
config: Res<GameConfig>,
|
|
||||||
mut windows: Query<&mut Window>,
|
|
||||||
) {
|
|
||||||
if config.is_changed() {
|
if config.is_changed() {
|
||||||
if let Ok(mut window) = windows.single_mut() {
|
if let Ok(mut window) = windows.single_mut() {
|
||||||
window.present_mode = match config.display.vsync {
|
window.present_mode = match config.display.vsync {
|
||||||
|
|||||||
@@ -151,14 +151,10 @@ impl TilemapBenchmark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Runs every frame — samples entity counts and frame timing.
|
/// Runs every frame — samples entity counts and frame timing.
|
||||||
pub fn track_benchmark(
|
pub fn track_benchmark(mut bench: ResMut<TilemapBenchmark>, time: Res<Time>) {
|
||||||
mut bench: ResMut<TilemapBenchmark>,
|
|
||||||
time: Res<Time>,
|
|
||||||
floor_tiles: Query<(), With<super::FloorTile>>,
|
|
||||||
) {
|
|
||||||
bench.frame_count += 1;
|
bench.frame_count += 1;
|
||||||
bench.frame_times.push(time.delta());
|
bench.frame_times.push(time.delta());
|
||||||
bench.floor_tile_count = floor_tiles.iter().count() as u32;
|
bench.floor_tile_count = 0;
|
||||||
|
|
||||||
// Trim rolling window to last 300 frames for p99 calculation
|
// Trim rolling window to last 300 frames for p99 calculation
|
||||||
if bench.frame_times.len() > 300 {
|
if bench.frame_times.len() > 300 {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
pub mod benchmark;
|
pub mod benchmark;
|
||||||
pub mod chunk_data;
|
pub mod chunk_data;
|
||||||
pub mod components;
|
|
||||||
pub mod prefabs;
|
|
||||||
pub mod rendering;
|
pub mod rendering;
|
||||||
pub mod tilemap;
|
pub mod tilemap;
|
||||||
pub mod tilemap_chunk;
|
pub mod tilemap_chunk;
|
||||||
@@ -9,8 +7,6 @@ pub mod visibility;
|
|||||||
|
|
||||||
pub use benchmark::*;
|
pub use benchmark::*;
|
||||||
pub use chunk_data::*;
|
pub use chunk_data::*;
|
||||||
pub use components::*;
|
|
||||||
pub use prefabs::*;
|
|
||||||
pub use tilemap::*;
|
pub use tilemap::*;
|
||||||
pub use tilemap_chunk::*;
|
pub use tilemap_chunk::*;
|
||||||
pub use visibility::*;
|
pub use visibility::*;
|
||||||
|
|||||||
+4
-195
@@ -1,195 +1,4 @@
|
|||||||
use bevy::prelude::*;
|
// Prefab types (FloorTilePrefab, FixtureTilePrefab) were deleted — they were
|
||||||
|
// entirely dead code. FloorTile and FixtureTile ECS components (in the former
|
||||||
use crate::{
|
// components.rs) were also never read. The TileMap + ChunkData bitsets are the
|
||||||
config::TileRegistry,
|
// sole source of truth for standability.
|
||||||
world::{
|
|
||||||
tiles::{FixtureTile, FloorTile, TileState},
|
|
||||||
FIXTURE_ID_OFFSET,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Bundle)]
|
|
||||||
pub struct FloorTilePrefab {
|
|
||||||
transform: Transform,
|
|
||||||
tile: FloorTile,
|
|
||||||
tile_state: TileState,
|
|
||||||
visibility: Visibility,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FloorTilePrefab {
|
|
||||||
pub fn grass(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().floor("grass");
|
|
||||||
FloorTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FloorTile {
|
|
||||||
id: def.id as u32,
|
|
||||||
opaque: !def.transparent,
|
|
||||||
walkable: def.can_stand_on,
|
|
||||||
astar_weight: def.astar_weight,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
tile_state: TileState {
|
|
||||||
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dirt(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().floor("dirt");
|
|
||||||
FloorTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FloorTile {
|
|
||||||
id: def.id as u32,
|
|
||||||
opaque: !def.transparent,
|
|
||||||
walkable: def.can_stand_on,
|
|
||||||
astar_weight: def.astar_weight,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
tile_state: TileState {
|
|
||||||
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rock(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().floor("rock");
|
|
||||||
FloorTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FloorTile {
|
|
||||||
id: def.id as u32,
|
|
||||||
opaque: !def.transparent,
|
|
||||||
walkable: def.can_stand_on,
|
|
||||||
astar_weight: def.astar_weight,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
tile_state: TileState {
|
|
||||||
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn air(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().floor("air");
|
|
||||||
FloorTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FloorTile {
|
|
||||||
id: def.id as u32,
|
|
||||||
opaque: !def.transparent,
|
|
||||||
walkable: def.can_stand_on,
|
|
||||||
astar_weight: def.astar_weight,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
tile_state: TileState {
|
|
||||||
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bedrock(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().floor("bedrock");
|
|
||||||
FloorTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FloorTile {
|
|
||||||
id: def.id as u32,
|
|
||||||
opaque: !def.transparent,
|
|
||||||
walkable: def.can_stand_on,
|
|
||||||
astar_weight: def.astar_weight,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
tile_state: TileState {
|
|
||||||
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn(self, commands: &mut Commands) -> Entity {
|
|
||||||
commands
|
|
||||||
.spawn((self.tile, self.transform, self.tile_state, self.visibility))
|
|
||||||
.id()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Bundle)]
|
|
||||||
pub struct FixtureTilePrefab {
|
|
||||||
transform: Transform,
|
|
||||||
tile: FixtureTile,
|
|
||||||
visibility: Visibility,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FixtureTilePrefab {
|
|
||||||
pub fn dirt_wall(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().fixture("dirt_wall");
|
|
||||||
FixtureTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FixtureTile {
|
|
||||||
id: FIXTURE_ID_OFFSET + def.id,
|
|
||||||
solid: def.solid,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rock_wall(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().fixture("rock_wall");
|
|
||||||
FixtureTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FixtureTile {
|
|
||||||
id: FIXTURE_ID_OFFSET + def.id,
|
|
||||||
solid: def.solid,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().fixture("log");
|
|
||||||
FixtureTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FixtureTile {
|
|
||||||
id: FIXTURE_ID_OFFSET + def.id,
|
|
||||||
solid: def.solid,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn leaves(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().fixture("leaves");
|
|
||||||
FixtureTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FixtureTile {
|
|
||||||
id: FIXTURE_ID_OFFSET + def.id,
|
|
||||||
solid: def.solid,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bedrock_wall(position: Vec3) -> Self {
|
|
||||||
let def = TileRegistry::global().fixture("bedrock_wall");
|
|
||||||
FixtureTilePrefab {
|
|
||||||
transform: Transform::from_translation(position),
|
|
||||||
tile: FixtureTile {
|
|
||||||
id: FIXTURE_ID_OFFSET + def.id,
|
|
||||||
solid: def.solid,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
},
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn(self, commands: &mut Commands) -> Entity {
|
|
||||||
commands
|
|
||||||
.spawn((self.tile, self.transform, self.visibility))
|
|
||||||
.id()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -237,6 +237,24 @@ impl TileMap {
|
|||||||
.unwrap_or(100)
|
.unwrap_or(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets.
|
||||||
|
/// BOTH must be cleared — leaving ChunkData stale causes is_standable bugs.
|
||||||
|
pub fn remove_fixture(&mut self, pos: &IVec3) -> Option<FixtureTileData> {
|
||||||
|
use super::chunk_data::ChunkData;
|
||||||
|
|
||||||
|
let removed = self.fixture_tiles.remove(pos);
|
||||||
|
let chunk_pos = world_to_chunk(*pos);
|
||||||
|
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
||||||
|
let (lx, ly, z) = ChunkData::world_to_local(*pos);
|
||||||
|
let idx = ChunkData::pos_to_index(lx, ly, z);
|
||||||
|
let word = idx / 32;
|
||||||
|
let clear_mask = !(1u32 << (idx % 32));
|
||||||
|
chunk.stand_in_fixture[word] &= clear_mask;
|
||||||
|
chunk.stand_on_fixture[word] &= clear_mask;
|
||||||
|
}
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove all tile data for a specific chunk from the TileMap.
|
/// Remove all tile data for a specific chunk from the TileMap.
|
||||||
/// Iterates all positions in the chunk volume and removes from HashMaps.
|
/// Iterates all positions in the chunk volume and removes from HashMaps.
|
||||||
/// Used during chunk unloading to clean up tile data.
|
/// Used during chunk unloading to clean up tile data.
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ pub fn spawn_tilemap_chunks(
|
|||||||
let half_chunk = (CHUNK_SIZE_TILE as f32) / 2.0 - TILE_SIZE / 2.0;
|
let half_chunk = (CHUNK_SIZE_TILE as f32) / 2.0 - TILE_SIZE / 2.0;
|
||||||
let world_x = (key.chunk_pos.x as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
|
let world_x = (key.chunk_pos.x as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
|
||||||
let world_y = (key.chunk_pos.y as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
|
let world_y = (key.chunk_pos.y as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
|
||||||
let z_depth = -(Z_BELOW - key.z_index as f32) * TILE_SIZE;
|
let z_depth = (key.z_index as f32 - Z_BELOW) * TILE_SIZE;
|
||||||
|
|
||||||
let visible = z_diff >= 0 && z_diff <= 8;
|
let visible = z_diff >= 0 && z_diff <= 8;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user