Implement async job pathfinding system
- Add calculate_traversal_distance() - full A* that returns actual distance - Add batch_calculate_traversals() for efficient batch processing - Add Calculating state to JobState enum for dorf locking - Add approach_target and retry_count to job Entry - Add helper methods: is_dorf_locked, get_locked_dorfs, get_scope_for_job - Add job_assignment config section with max_dorfs_per_job - Create job_pathfinding.rs with main pathfinding system: - Throttled to MAX_JOBS_PER_TICK (5) per frame - Expanding scope: 5 -> 20 -> 200 dorfs based on retry count - Uses full pathfinding, not provisional - Pre-computes approach_target for assigned dorfs - Simplify job_assignment.rs to only handle idle fallback - Update has_fell_tree to check state (Unclaimed, Calculating, Claimed)
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
use bevy::prelude::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::config::GameConfig;
|
||||
use crate::entities::behaviour::EntityType;
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::shared_systems::pathfinding::calculate_traversal_distance;
|
||||
use crate::entities::tasks::components::{ChopStep, HaulStep, Task, TaskQueue, TaskState};
|
||||
use crate::entities::tasks::job_queue::{JobId, JobKind, JobQueue, JobState};
|
||||
use crate::world::tiles::TileMap;
|
||||
|
||||
const MAX_JOBS_PER_TICK: usize = 5;
|
||||
|
||||
pub fn job_pathfinding_system(
|
||||
mut job_queue: ResMut<JobQueue>,
|
||||
config: Res<GameConfig>,
|
||||
tilemap: Res<TileMap>,
|
||||
mut dorf_query: Query<
|
||||
(
|
||||
Entity,
|
||||
&mut TaskQueue,
|
||||
&mut TaskState,
|
||||
&Transform,
|
||||
&mut Ambulatory,
|
||||
),
|
||||
With<EntityType>,
|
||||
>,
|
||||
) {
|
||||
let mut unclaimed: Vec<usize> = job_queue.iter_unclaimed().map(|(idx, _)| idx).collect();
|
||||
|
||||
if unclaimed.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
unclaimed.sort_by(|&a, &b| {
|
||||
let pri_a = job_queue.get_job_priority(a).unwrap_or(0);
|
||||
let pri_b = job_queue.get_job_priority(b).unwrap_or(0);
|
||||
pri_b.cmp(&pri_a)
|
||||
});
|
||||
|
||||
let locked_dorfs = job_queue.get_locked_dorfs();
|
||||
|
||||
for job_idx in unclaimed.into_iter().take(MAX_JOBS_PER_TICK) {
|
||||
let (kind, state, _) = match job_queue.get_job_at(job_idx) {
|
||||
Some(k) => k,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if !matches!(state, JobState::Unclaimed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let kind = kind.clone();
|
||||
let target_pos = kind.target();
|
||||
let scope = job_queue.get_scope_for_job(job_idx);
|
||||
|
||||
let mut candidate_dorfs: Vec<(Entity, IVec3)> = dorf_query
|
||||
.iter_mut()
|
||||
.filter(|(entity, queue, state, transform, _)| {
|
||||
if locked_dorfs.contains(entity) {
|
||||
return false;
|
||||
}
|
||||
let pos = transform.translation.as_ivec3();
|
||||
if !tilemap.is_standable(pos) {
|
||||
return false;
|
||||
}
|
||||
if !queue.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let state_val: &TaskState = &*state;
|
||||
if matches!(state_val, TaskState::Active) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.map(|(entity, _, _, transform, _)| (entity, transform.translation.as_ivec3()))
|
||||
.collect();
|
||||
|
||||
candidate_dorfs.sort_by_key(|(_, pos)| {
|
||||
(pos.x - target_pos.x).abs()
|
||||
+ (pos.y - target_pos.y).abs()
|
||||
+ (pos.z - target_pos.z).abs()
|
||||
});
|
||||
|
||||
let dorfs_to_try: Vec<_> = candidate_dorfs.into_iter().take(scope as usize).collect();
|
||||
|
||||
if dorfs_to_try.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dorf_entities: Vec<Entity> = dorfs_to_try.iter().map(|(e, _)| *e).collect();
|
||||
if !job_queue.set_job_calculating(job_idx, dorf_entities.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut best_result: Option<(Entity, IVec3, i32)> = None;
|
||||
|
||||
match kind {
|
||||
JobKind::FellTree { trunk_pos } => {
|
||||
let approach_tiles = find_all_standable_adjacent(&trunk_pos, &tilemap);
|
||||
if approach_tiles.is_empty() {
|
||||
job_queue.suspend_job(job_idx);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (dorf_entity, dorf_pos) in &dorfs_to_try {
|
||||
let mut best_for_dorf: Option<(IVec3, i32)> = None;
|
||||
|
||||
for approach_tile in &approach_tiles {
|
||||
if let Ok(distance) =
|
||||
calculate_traversal_distance(&tilemap, *dorf_pos, *approach_tile)
|
||||
{
|
||||
match best_for_dorf {
|
||||
None => best_for_dorf = Some((*approach_tile, distance)),
|
||||
Some((_, best_dist)) if distance < best_dist => {
|
||||
best_for_dorf = Some((*approach_tile, distance));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((approach, dist)) = best_for_dorf {
|
||||
match best_result {
|
||||
None => best_result = Some((*dorf_entity, approach, dist)),
|
||||
Some((_, _, best_dist)) if dist < best_dist => {
|
||||
best_result = Some((*dorf_entity, approach, dist));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
JobKind::HaulCargo { cargo_pos, .. } => {
|
||||
for (dorf_entity, dorf_pos) in &dorfs_to_try {
|
||||
if let Ok(distance) =
|
||||
calculate_traversal_distance(&tilemap, *dorf_pos, cargo_pos)
|
||||
{
|
||||
match best_result {
|
||||
None => best_result = Some((*dorf_entity, cargo_pos, distance)),
|
||||
Some((_, _, best_dist)) if distance < best_dist => {
|
||||
best_result = Some((*dorf_entity, cargo_pos, distance));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((dorf_entity, approach_target, _)) = best_result {
|
||||
if let Some(job_id) = job_queue.assign_job(job_idx, dorf_entity, approach_target) {
|
||||
if let Ok((_, mut queue, mut state, _, mut ambulatory)) =
|
||||
dorf_query.get_mut(dorf_entity)
|
||||
{
|
||||
let job_kind = match job_queue.get_job_kind_at(job_idx) {
|
||||
Some(k) => k,
|
||||
None => continue,
|
||||
};
|
||||
let task = match job_kind {
|
||||
JobKind::FellTree { trunk_pos } => Task::ChopTree {
|
||||
job_id,
|
||||
trunk_pos,
|
||||
chop_ticks: 120,
|
||||
step: ChopStep::MovingToTree {
|
||||
approach: Some(approach_target),
|
||||
},
|
||||
},
|
||||
JobKind::HaulCargo {
|
||||
cargo_entity,
|
||||
cargo_pos,
|
||||
dest,
|
||||
} => Task::HaulCargo {
|
||||
job_id,
|
||||
cargo_entity,
|
||||
cargo_pos,
|
||||
dest,
|
||||
step: HaulStep::MovingToCargo {
|
||||
approach: Some(approach_target),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
queue.clear();
|
||||
queue.push(task);
|
||||
*state = TaskState::Pending;
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.target = None;
|
||||
ambulatory.path_index = 0;
|
||||
|
||||
info!(
|
||||
"[PATHFIND] Assigned job {:?} to dorf {:?} with approach {:?}",
|
||||
job_id, dorf_entity, approach_target
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
job_queue.increment_retry(job_idx);
|
||||
if !dorf_entities.is_empty() {
|
||||
job_queue.unclaim_jobs_for_entity(dorf_entities[0]);
|
||||
}
|
||||
let retry_count = job_queue.get_job_retry_count(job_idx).unwrap_or(0);
|
||||
if retry_count >= 10 {
|
||||
job_queue.suspend_job(job_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_all_standable_adjacent(trunk_pos: &IVec3, tilemap: &TileMap) -> SmallVec<[IVec3; 8]> {
|
||||
use crate::constants::ITILE_SIZE;
|
||||
let mut tiles = SmallVec::new();
|
||||
let standing_z = trunk_pos.z;
|
||||
|
||||
for dx in -1i32..=1 {
|
||||
for dy in -1i32..=1 {
|
||||
if dx == 0 && dy == 0 {
|
||||
continue;
|
||||
}
|
||||
let candidate = IVec3::new(
|
||||
trunk_pos.x + dx * ITILE_SIZE,
|
||||
trunk_pos.y + dy * ITILE_SIZE,
|
||||
standing_z,
|
||||
);
|
||||
|
||||
if tilemap.is_standable(candidate) {
|
||||
tiles.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
tiles
|
||||
}
|
||||
Reference in New Issue
Block a user