211 lines
7.0 KiB
Rust
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
|
|
}
|