Files
dorf/src/entities/tasks/idle.rs
T
2026-03-22 12:56:52 +00:00

211 lines
7.0 KiB
Rust

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, IDLE_MAX_RETRIES};
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,
retry_count,
} => {
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_count += 1;
if *retry_count >= IDLE_MAX_RETRIES {
panic!(
"Entity stuck: pick_gaussian_target returned None {} times \
consecutively. origin={:?} sigma_world={:.1} \
loaded_chunks={} \
— no standable tile found. Check tilemap state.",
retry_count,
origin,
sigma_world,
chunk_map.loaded_chunks.len(),
);
}
*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,
retry_count: 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,
retry_count: 0,
};
} else {
*ticks_remaining -= 1;
}
}
}
}
fn pick_gaussian_target(
origin: &IVec3,
sigma_world: f32,
tilemap: &TileMap,
chunk_map: &ChunkMap,
rng: &mut WyRand,
) -> Option<IVec3> {
let two_sigma_sq = 2.0 * sigma_world * sigma_world;
let mut chosen: Option<IVec3> = 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<IVec3> {
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
}