The target oscillation was caused by job_pathfinding_system
clearing ambulatory.target = None on assignment, then the executor
setting it to approach, then job_pathfinding clearing it again
the next frame.
By not clearing ambulatory.target, the executor sets it once
and it persists until the task completes.
The bug: We excluded ALL Active state dorfs, but Idle tasks get
immediately promoted from Pending to Active by task_executor_system.
So by the time job_pathfinding runs, all dorfs are Active with Idle.
The fix: Check if dorf is 'busy' = Active AND non-Idle task.
Dorfs with Idle tasks (even in Active state) can be interrupted
for real jobs.
KEY FIX: Changed candidate filter to allow dorfs with Idle tasks:
- Before: Only dorfs with empty queues were candidates
- After: Dorfs with empty queue OR Idle task are candidates
- This fixes deadlock where all dorfs had Idle tasks and were excluded
Debug logging added:
- job_pathfinding.rs: Log job processing, candidate counts, path results
- queue_debug.rs: Show job state (Unclaimed/Calculating/Claimed/Suspended), retry count, and dorf idle status
The bug was that job_pathfinding_system filtered out ALL dorfs because:
1. job_assignment_system gave Idle tasks to dorfs with empty queues
2. task_executor_system promoted them to Active
3. job_pathfinding_system then excluded Active dorfs AND non-empty queues
- 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)
- Add JobState enum (Unclaimed, Claimed, Complete) replacing boolean claimed flag
- Increase provisional node limit from 256 to 4096
- Add iter_unclaimed() and claim_job_at() methods to JobQueue
- Update job_assignment to filter idle dorfs, match by distance
- Add unclaim_jobs_for_entity() for handling dorf death/failure
- Add JobId collision detection in debug builds
- Fix demo system to check Pending state for chopping dorfs
- Throttle queue_debug to every 120 ticks
- Add surface position deduplication for terrain chunks
- Add JobId (rolling u16 counter) to track jobs through their lifecycle
- Add job_id field to all Task variants (Idle, GoTo, ChopTree, HaulCargo, DropHauled)
- Add unclaim_job() method to requeue failed jobs for reassignment
- Change task failure paths to use unclaim_job() instead of complete_job_by_id()
- This allows failed jobs to be picked up by another dorf instead of being stuck
- Add to_job_kind() and get_job_id() helper methods to Task
- Fix pathfinding is_standable_tile debug logging spam
- Fix chunk bounds check in ChunkData::is_standable (was checking wrong z range)