use crate::constants::{ITILE_SIZE, TILE_SIZE}; use crate::entities::behaviour::IdleBehaviour; use crate::entities::shared_components::Ambulatory; use crate::entities::tasks::components::{IdleState, Task}; use crate::world::chunks::ChunkMap; use crate::world::chunks::CHUNK_SIZE; use crate::world::tiles::TileMap; use bevy::prelude::*; use bevy_rand::prelude::*; use rand::RngExt; pub(super) fn execute_idle( task: &mut Task, transform: &Transform, ambulatory: &mut Ambulatory, sprite: &mut Sprite, tilemap: &TileMap, chunk_map: &ChunkMap, behaviour: &IdleBehaviour, rng: &mut WyRand, current_tick: u32, ) { let Task::Idle { origin, sigma_world, state, } = task else { return; }; match state { IdleState::Picking { retry_after_tick } => { if current_tick < *retry_after_tick { return; } match pick_gaussian_target(origin, *sigma_world, tilemap, chunk_map, rng) { Some(target) => { ambulatory.target = Some(Vec3::new( target.x as f32, target.y as f32, target.z as f32 + 1.0, )); ambulatory.current_path = None; ambulatory.path_index = 0; *state = IdleState::Moving { target }; } None => { *retry_after_tick = current_tick.saturating_add(30); } } } IdleState::Moving { target } => { let dx = transform.translation.x - target.x as f32; let dy = transform.translation.y - target.y as f32; let dist_sq = dx * dx + dy * dy; let arrive_threshold_sq = (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5); let arrived = dist_sq < arrive_threshold_sq; let no_nav = ambulatory.target.is_none() && ambulatory.current_path.is_none(); if arrived || no_nav { ambulatory.target = None; ambulatory.current_path = None; let loiter_roll: f32 = rng.random(); if loiter_roll < behaviour.loiter_chance { let duration_range = behaviour.loiter_max_ticks - behaviour.loiter_min_ticks; let duration = behaviour.loiter_min_ticks + rng.random_range(0..=duration_range); let flip1: f32 = rng.random(); let flip2: f32 = rng.random(); let flips_remaining = (flip1 < behaviour.flip_chance) as u8 + (flip2 < behaviour.flip_chance) as u8; let next_flip_at = if flips_remaining > 0 { current_tick.saturating_add(rng.random_range(1..=duration / 2)) } else { u32::MAX }; *state = IdleState::Loitering { ticks_remaining: duration, flips_remaining, next_flip_at, }; } else { *state = IdleState::Picking { retry_after_tick: 0, }; } } } IdleState::Loitering { ticks_remaining, flips_remaining, next_flip_at, } => { if *flips_remaining > 0 && current_tick >= *next_flip_at { sprite.flip_x = !sprite.flip_x; *flips_remaining -= 1; if *flips_remaining > 0 && *ticks_remaining > 2 { *next_flip_at = current_tick.saturating_add(rng.random_range(1..=*ticks_remaining / 2)); } } if *ticks_remaining == 0 { sprite.flip_x = false; *state = IdleState::Picking { retry_after_tick: 0, }; } else { *ticks_remaining -= 1; } } } } fn pick_gaussian_target( origin: &IVec3, sigma_world: f32, tilemap: &TileMap, chunk_map: &ChunkMap, rng: &mut WyRand, ) -> Option { let two_sigma_sq = 2.0 * sigma_world * sigma_world; let mut chosen: Option = None; let mut weight_sum = 0.0f32; for &chunk_pos in chunk_map.loaded_chunks.keys() { let has_all_neighbours = chunk_map .loaded_chunks .contains_key(&(chunk_pos + IVec2::X)) && chunk_map .loaded_chunks .contains_key(&(chunk_pos - IVec2::X)) && chunk_map .loaded_chunks .contains_key(&(chunk_pos + IVec2::Y)) && chunk_map .loaded_chunks .contains_key(&(chunk_pos - IVec2::Y)); if !has_all_neighbours { continue; } const SAMPLES_PER_CHUNK: usize = 4; for _ in 0..SAMPLES_PER_CHUNK { let local_x = rng.random_range(0..CHUNK_SIZE); let local_y = rng.random_range(0..CHUNK_SIZE); let world_x = (chunk_pos.x * CHUNK_SIZE + local_x) * ITILE_SIZE; let world_y = (chunk_pos.y * CHUNK_SIZE + local_y) * ITILE_SIZE; let Some(candidate) = find_surface(world_x, world_y, tilemap) else { continue; }; let dx = (candidate.x - origin.x) as f32; let dy = (candidate.y - origin.y) as f32; let dist_sq = dx * dx + dy * dy; let weight = (-dist_sq / two_sigma_sq).exp(); weight_sum += weight; let accept: f32 = rng.random(); if accept < weight / weight_sum { chosen = Some(candidate); } } } chosen } #[inline] fn find_surface(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option { for z in -3i32..=4i32 { let floor_pos = IVec3::new(world_x, world_y, z * ITILE_SIZE); if tilemap.floor_tiles.contains_key(&floor_pos) { let above = IVec3::new(world_x, world_y, floor_pos.z + ITILE_SIZE); if tilemap.is_standable(above) { return Some(above); } } } None }