//! Task executor system — the dispatcher that drives entity behaviour. //! //! Runs in FixedUpdate. For each entity with TaskQueue: //! 1. If no active task, pop front of queue → set TaskState::Active //! 2. Match on active Task variant, execute corresponding logic //! 3. On completion/failure, update TaskState, fire event, pop task //! 4. If queue empty after pop, assign default Task::Idle //! //! Uses Changed + Changed to minimise queries. use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType}; use crate::entities::cargo::{Cargo, HaulSlot}; use crate::entities::shared_components::Ambulatory; use crate::entities::tasks::components::{ ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState, }; use crate::entities::tasks::events::{TaskClaimed, TaskCompleted, TaskFailed}; use crate::entities::tasks::idle::execute_idle; use crate::world::chunks::ChunkMap; use crate::world::generation::forestry::{fell_tree, TreePart}; use crate::world::tiles::tile_changed::TileChangedEvent; use crate::world::tiles::visibility::TileOcclusionEvent; use crate::world::tiles::TileMap; use bevy::prelude::*; use bevy_rand::prelude::*; /// Main task executor. Runs in FixedUpdate. pub fn task_executor_system( mut commands: Commands, mut tilemap_mut: ResMut, chunk_map: Res, asset_server: Res, mut rng_q: Query<&mut WyRand, With>, mut tick: Local, tree_parts: Query<(Entity, &TreePart)>, mut cargo_query: Query<&mut Cargo>, mut query: Query<( Entity, &mut TaskQueue, &mut TaskState, &mut Ambulatory, &Transform, &mut Sprite, &EntityType, Option<&mut HaulSlot>, )>, mut claimed_writer: MessageWriter, mut completed_writer: MessageWriter, mut failed_writer: MessageWriter, mut tile_changed: MessageWriter, mut occlusion: MessageWriter, ) { let Ok(mut rng) = rng_q.single_mut() else { return; }; *tick = tick.wrapping_add(1); let current_tick = *tick; for ( entity, mut queue, mut state, mut ambulatory, transform, mut sprite, entity_type, mut haul_slot, ) in query.iter_mut() { let origin = transform.translation.as_ivec3(); let behaviour = EntityBehaviourRegistry::global_get(entity_type.0); if queue.is_empty() { queue.push(Task::Idle { origin, sigma_world: behaviour.idle.sigma_world, state: IdleState::Picking { retry_after_tick: 0, retry_count: 0, }, }); } // Promote Pending → Active if needed if *state == TaskState::Pending && !queue.is_empty() { *state = TaskState::Active; if let Some(task) = queue.current() { claimed_writer.write(TaskClaimed { entity, task: task.clone(), }); } } // Execute current task if Active if *state == TaskState::Active { if let Some(current_task) = queue.current_mut() { match current_task { Task::Idle { .. } => { execute_idle( current_task, transform, &mut ambulatory, &mut sprite, &*tilemap_mut, &chunk_map, &behaviour.idle, &mut rng, current_tick, ); } Task::GoTo { target, threshold_tiles, } => { let entity_pos = (transform.translation / 16.0f32).as_ivec3(); let distance = (entity_pos.x - target.x) .abs() .max((entity_pos.y - target.y).abs()); if distance <= *threshold_tiles || !tilemap_mut.is_standable(*target) { *state = TaskState::Completed; } else { ambulatory.target = Some(Vec3::new( target.x as f32, target.y as f32, target.z as f32 + 1.0, )); ambulatory.current_path = None; } } Task::ChopTree { trunk_pos, chop_ticks, step, } => match step { ChopStep::MovingToTree => { if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) { failed_writer.write(TaskFailed { entity, task: current_task.clone(), reason: "tree already gone", }); *state = TaskState::Failed; continue; } if ambulatory.target.is_none() { ambulatory.target = Some(Vec3::new( trunk_pos.x as f32, trunk_pos.y as f32, trunk_pos.z as f32 + 1.0, )); ambulatory.current_path = None; } let dx = transform.translation.x - trunk_pos.x as f32; let dy = transform.translation.y - trunk_pos.y as f32; let dist_sq = dx * dx + dy * dy; let chop_range_sq = (crate::constants::TILE_SIZE * 2.5) * (crate::constants::TILE_SIZE * 2.5); if dist_sq <= chop_range_sq { ambulatory.target = None; ambulatory.current_path = None; *step = ChopStep::Chopping { ticks_remaining: *chop_ticks, }; } } ChopStep::Chopping { ticks_remaining } => { if *ticks_remaining == 0 { let trunk_positions = fell_tree( *trunk_pos, &tree_parts, &mut commands, &mut tilemap_mut, &mut tile_changed, &mut occlusion, ); let log_sprite = asset_server.load("tree_trunk.png"); for &pos in trunk_positions.iter() { crate::entities::cargo::spawn_log_cargo( &mut commands, &mut tilemap_mut, pos, log_sprite.clone(), ); } *step = ChopStep::Done; } else { *ticks_remaining -= 1; } } ChopStep::Done => { *state = TaskState::Completed; } }, Task::HaulCargo { cargo_entity, cargo_pos, dest, step, } => { let Some(ref mut haul) = haul_slot else { failed_writer.write(TaskFailed { entity, task: current_task.clone(), reason: "entity has no HaulSlot", }); *state = TaskState::Failed; continue; }; match step { HaulStep::MovingToCargo => { if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) { failed_writer.write(TaskFailed { entity, task: current_task.clone(), reason: "cargo no longer exists", }); *state = TaskState::Failed; continue; } if ambulatory.target.is_none() { ambulatory.target = Some(Vec3::new( cargo_pos.x as f32, cargo_pos.y as f32, cargo_pos.z as f32 + 1.0, )); ambulatory.current_path = None; } let dx = transform.translation.x - cargo_pos.x as f32; let dy = transform.translation.y - cargo_pos.y as f32; let dist_sq = dx * dx + dy * dy; let pickup_range_sq = (crate::constants::TILE_SIZE * 1.5) * (crate::constants::TILE_SIZE * 1.5); if dist_sq <= pickup_range_sq { ambulatory.target = None; *step = HaulStep::PickingUp; } } HaulStep::PickingUp => { if haul.is_occupied() { failed_writer.write(TaskFailed { entity, task: current_task.clone(), reason: "HaulSlot already occupied", }); *state = TaskState::Failed; continue; } if let Some(_) = tilemap_mut.remove_cargo(cargo_pos) { haul.pick_up(*cargo_entity); *step = HaulStep::MovingToDest { chosen_drop: None }; } else { failed_writer.write(TaskFailed { entity, task: current_task.clone(), reason: "cargo vanished before pickup", }); *state = TaskState::Failed; } } HaulStep::MovingToDest { chosen_drop } => { if chosen_drop.is_none() { *chosen_drop = Some( tilemap_mut .find_nearest_free_cargo_tile(*dest, 8) .unwrap_or(*dest), ); } let drop_pos = chosen_drop.unwrap(); if ambulatory.target.is_none() { ambulatory.target = Some(Vec3::new( drop_pos.x as f32, drop_pos.y as f32, drop_pos.z as f32 + 1.0, )); ambulatory.current_path = None; } let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO); let dx = transform.translation.x - target_pos.x; let dy = transform.translation.y - target_pos.y; let dist_sq = dx * dx + dy * dy; let arrive_sq = (crate::constants::TILE_SIZE * 1.5) * (crate::constants::TILE_SIZE * 1.5); if dist_sq <= arrive_sq { ambulatory.target = None; *step = HaulStep::Dropping { drop_pos }; } } HaulStep::Dropping { drop_pos } => { if let Some(cargo_entity) = haul.release() { let _ = tilemap_mut.place_cargo(*drop_pos, cargo_entity); if let Ok(mut cargo) = cargo_query.get_mut(cargo_entity) { cargo.tile_pos = *drop_pos; } } *step = HaulStep::Done; } HaulStep::Done => { *state = TaskState::Completed; } } } Task::DropHauled { pos, step } => { let Some(ref mut haul) = haul_slot else { *state = TaskState::Completed; continue; }; match step { DropStep::Moving { chosen_drop } => { if chosen_drop.is_none() { *chosen_drop = Some( tilemap_mut .find_nearest_free_cargo_tile(*pos, 8) .unwrap_or(*pos), ); } let drop_pos = chosen_drop.unwrap(); if ambulatory.target.is_none() { ambulatory.target = Some(Vec3::new( drop_pos.x as f32, drop_pos.y as f32, drop_pos.z as f32 + 1.0, )); ambulatory.current_path = None; } let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO); let dx = transform.translation.x - target_pos.x; let dy = transform.translation.y - target_pos.y; let dist_sq = dx * dx + dy * dy; let arrive_sq = (crate::constants::TILE_SIZE * 1.5) * (crate::constants::TILE_SIZE * 1.5); if dist_sq <= arrive_sq { ambulatory.target = None; *step = DropStep::Dropping { drop_pos }; } } DropStep::Dropping { drop_pos } => { if let Some(cargo_entity) = haul.release() { let _ = tilemap_mut.place_cargo(*drop_pos, cargo_entity); if let Ok(mut cargo) = cargo_query.get_mut(cargo_entity) { cargo.tile_pos = *drop_pos; } } *step = DropStep::Done; } DropStep::Done => { *state = TaskState::Completed; } } } } } } // Handle task completion/failure if *state == TaskState::Completed || *state == TaskState::Failed { if let Some(completed_task) = queue.pop() { if *state == TaskState::Completed { completed_writer.write(TaskCompleted { entity, task: completed_task.clone(), }); } else { failed_writer.write(TaskFailed { entity, task: completed_task.clone(), reason: completed_task.name(), }); } if completed_task.is_terminal() && queue.is_empty() { queue.push(Task::Idle { origin, sigma_world: behaviour.idle.sigma_world, state: IdleState::Picking { retry_after_tick: 0, retry_count: 0, }, }); } } *state = TaskState::Pending; } } }